TweetFilter AI

A highly customizable AI rates tweets 1-10 and removes all the slop, saving your braincells!

当前为 2025-04-10 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name TweetFilter AI
  3. // @namespace http://tampermonkey.net/
  4. // @version Version 1.3
  5. // @description A highly customizable AI rates tweets 1-10 and removes all the slop, saving your braincells!
  6. // @author Obsxrver(3than)
  7. // @match *://twitter.com/*
  8. // @match *://x.com/*
  9. // @grant GM_addStyle
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_getResourceText
  14. // @connect openrouter.ai
  15. // @run-at document-idle
  16. // @license MIT
  17. // ==/UserScript==
  18. (function() {
  19. 'use strict';
  20. console.log("X/Twitter Tweet De-Sloppification Activated (Combined Version)");
  21.  
  22. // Embedded Menu.html
  23. const MENU = `
  24. <style>
  25. /*
  26. Modern X-Inspired Styles - Enhanced
  27. ---------------------------------
  28. */
  29.  
  30. /* Main tweet filter container */
  31. #tweet-filter-container {
  32. position: fixed;
  33. top: 70px;
  34. right: 15px;
  35. background-color: rgba(22, 24, 28, 0.95);
  36. color: #e7e9ea;
  37. padding: 10px 12px;
  38. border-radius: 12px;
  39. z-index: 9999;
  40. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  41. font-size: 13px;
  42. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
  43. display: flex;
  44. align-items: center;
  45. gap: 10px;
  46. border: 1px solid rgba(255, 255, 255, 0.1);
  47. }
  48.  
  49. /* Close button styles */
  50. .close-button {
  51. background: none;
  52. border: none;
  53. color: #e7e9ea;
  54. font-size: 16px;
  55. cursor: pointer;
  56. padding: 0;
  57. width: 28px;
  58. height: 28px;
  59. display: flex;
  60. align-items: center;
  61. justify-content: center;
  62. opacity: 0.8;
  63. transition: opacity 0.2s;
  64. border-radius: 50%;
  65. }
  66.  
  67. .close-button:hover {
  68. opacity: 1;
  69. background-color: rgba(255, 255, 255, 0.1);
  70. }
  71.  
  72. /* Hidden state */
  73. .hidden {
  74. display: none !important;
  75. }
  76.  
  77. /* Show/hide button */
  78. .toggle-button {
  79. position: fixed;
  80. right: 15px;
  81. background-color: rgba(22, 24, 28, 0.95);
  82. color: #e7e9ea;
  83. padding: 8px 12px;
  84. border-radius: 8px;
  85. cursor: pointer;
  86. font-size: 12px;
  87. z-index: 9999;
  88. border: 1px solid rgba(255, 255, 255, 0.1);
  89. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
  90. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  91. display: flex;
  92. align-items: center;
  93. gap: 6px;
  94. transition: all 0.2s ease;
  95. }
  96. .toggle-button:hover {
  97. background-color: rgba(29, 155, 240, 0.2);
  98. }
  99.  
  100. #filter-toggle {
  101. top: 70px;
  102. }
  103.  
  104. #settings-toggle {
  105. top: 120px;
  106. }
  107.  
  108. #tweet-filter-container label {
  109. margin: 0;
  110. font-weight: bold;
  111. }
  112.  
  113. #tweet-filter-slider {
  114. cursor: pointer;
  115. width: 120px;
  116. vertical-align: middle;
  117. accent-color: #1d9bf0;
  118. }
  119.  
  120. #tweet-filter-value {
  121. min-width: 20px;
  122. text-align: center;
  123. font-weight: bold;
  124. background-color: rgba(255, 255, 255, 0.1);
  125. padding: 2px 5px;
  126. border-radius: 4px;
  127. }
  128.  
  129. /* Settings UI with Tabs */
  130. #settings-container {
  131. position: fixed;
  132. top: 70px;
  133. right: 15px;
  134. background-color: rgba(22, 24, 28, 0.95);
  135. color: #e7e9ea;
  136. padding: 0; /* Remove padding to accommodate sticky header */
  137. border-radius: 16px;
  138. z-index: 9999;
  139. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  140. font-size: 13px;
  141. box-shadow: 0 2px 18px rgba(0, 0, 0, 0.6);
  142. display: flex;
  143. flex-direction: column;
  144. width: 380px;
  145. max-height: 85vh;
  146. overflow: hidden; /* Hide overflow to make the sticky header work properly */
  147. border: 1px solid rgba(255, 255, 255, 0.1);
  148. line-height: 1.3;
  149. transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  150. transform-origin: top right;
  151. }
  152. #settings-container.hidden {
  153. opacity: 0;
  154. transform: scale(0.9);
  155. pointer-events: none;
  156. }
  157. /* Header section */
  158. .settings-header {
  159. padding: 12px 15px;
  160. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  161. display: flex;
  162. justify-content: space-between;
  163. align-items: center;
  164. position: sticky;
  165. top: 0;
  166. background-color: rgba(22, 24, 28, 0.98);
  167. z-index: 20;
  168. border-radius: 16px 16px 0 0;
  169. }
  170. .settings-title {
  171. font-weight: bold;
  172. font-size: 16px;
  173. }
  174. /* Content area with scrolling */
  175. .settings-content {
  176. overflow-y: auto;
  177. max-height: calc(85vh - 110px); /* Account for header and tabs */
  178. padding: 0;
  179. }
  180. /* Scrollbar styling for settings container */
  181. .settings-content::-webkit-scrollbar {
  182. width: 6px;
  183. }
  184.  
  185. .settings-content::-webkit-scrollbar-track {
  186. background: rgba(255, 255, 255, 0.05);
  187. border-radius: 3px;
  188. }
  189.  
  190. .settings-content::-webkit-scrollbar-thumb {
  191. background: rgba(255, 255, 255, 0.2);
  192. border-radius: 3px;
  193. }
  194.  
  195. .settings-content::-webkit-scrollbar-thumb:hover {
  196. background: rgba(255, 255, 255, 0.3);
  197. }
  198.  
  199. /* Tab Navigation */
  200. .tab-navigation {
  201. display: flex;
  202. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  203. position: sticky;
  204. top: 0;
  205. background-color: rgba(22, 24, 28, 0.98);
  206. z-index: 10;
  207. padding: 10px 15px;
  208. gap: 8px;
  209. }
  210.  
  211. .tab-button {
  212. padding: 6px 10px;
  213. background: none;
  214. border: none;
  215. color: #e7e9ea;
  216. font-weight: bold;
  217. cursor: pointer;
  218. border-radius: 8px;
  219. transition: all 0.2s ease;
  220. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  221. font-size: 13px;
  222. flex: 1;
  223. text-align: center;
  224. }
  225.  
  226. .tab-button:hover {
  227. background-color: rgba(255, 255, 255, 0.1);
  228. }
  229.  
  230. .tab-button.active {
  231. color: #1d9bf0;
  232. background-color: rgba(29, 155, 240, 0.1);
  233. border-bottom: 2px solid #1d9bf0;
  234. }
  235.  
  236. /* Tab Content */
  237. .tab-content {
  238. display: none;
  239. animation: fadeIn 0.3s ease;
  240. padding: 15px;
  241. }
  242. @keyframes fadeIn {
  243. from { opacity: 0; }
  244. to { opacity: 1; }
  245. }
  246.  
  247. .tab-content.active {
  248. display: block;
  249. }
  250.  
  251. /* Enhanced dropdowns */
  252. .select-container {
  253. position: relative;
  254. margin-bottom: 15px;
  255. }
  256. .select-container .search-field {
  257. position: sticky;
  258. top: 0;
  259. background-color: rgba(39, 44, 48, 0.95);
  260. padding: 8px;
  261. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  262. z-index: 1;
  263. }
  264. .select-container .search-input {
  265. width: 100%;
  266. padding: 8px 10px;
  267. border-radius: 8px;
  268. border: 1px solid rgba(255, 255, 255, 0.2);
  269. background-color: rgba(39, 44, 48, 0.9);
  270. color: #e7e9ea;
  271. font-size: 12px;
  272. transition: border-color 0.2s;
  273. }
  274. .select-container .search-input:focus {
  275. border-color: #1d9bf0;
  276. outline: none;
  277. }
  278. .custom-select {
  279. position: relative;
  280. display: inline-block;
  281. width: 100%;
  282. }
  283. .select-selected {
  284. background-color: rgba(39, 44, 48, 0.95);
  285. color: #e7e9ea;
  286. padding: 10px 12px;
  287. border: 1px solid rgba(255, 255, 255, 0.2);
  288. border-radius: 8px;
  289. cursor: pointer;
  290. user-select: none;
  291. display: flex;
  292. justify-content: space-between;
  293. align-items: center;
  294. font-size: 13px;
  295. transition: border-color 0.2s;
  296. }
  297. .select-selected:hover {
  298. border-color: rgba(255, 255, 255, 0.4);
  299. }
  300. .select-selected:after {
  301. content: "";
  302. width: 8px;
  303. height: 8px;
  304. border: 2px solid #e7e9ea;
  305. border-width: 0 2px 2px 0;
  306. display: inline-block;
  307. transform: rotate(45deg);
  308. margin-left: 10px;
  309. transition: transform 0.2s;
  310. }
  311. .select-selected.select-arrow-active:after {
  312. transform: rotate(-135deg);
  313. }
  314. .select-items {
  315. position: absolute;
  316. background-color: rgba(39, 44, 48, 0.98);
  317. top: 100%;
  318. left: 0;
  319. right: 0;
  320. z-index: 99;
  321. max-height: 300px;
  322. overflow-y: auto;
  323. border: 1px solid rgba(255, 255, 255, 0.2);
  324. border-radius: 8px;
  325. margin-top: 5px;
  326. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
  327. display: none;
  328. }
  329. .select-items div {
  330. color: #e7e9ea;
  331. padding: 10px 12px;
  332. cursor: pointer;
  333. user-select: none;
  334. transition: background-color 0.2s;
  335. border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  336. }
  337. .select-items div:hover {
  338. background-color: rgba(29, 155, 240, 0.1);
  339. }
  340. .select-items div.same-as-selected {
  341. background-color: rgba(29, 155, 240, 0.2);
  342. }
  343. /* Scrollbar for select items */
  344. .select-items::-webkit-scrollbar {
  345. width: 6px;
  346. }
  347. .select-items::-webkit-scrollbar-track {
  348. background: rgba(255, 255, 255, 0.05);
  349. }
  350. .select-items::-webkit-scrollbar-thumb {
  351. background: rgba(255, 255, 255, 0.2);
  352. border-radius: 3px;
  353. }
  354. .select-items::-webkit-scrollbar-thumb:hover {
  355. background: rgba(255, 255, 255, 0.3);
  356. }
  357. /* Form elements */
  358. #openrouter-api-key,
  359. #user-instructions {
  360. width: 100%;
  361. padding: 10px 12px;
  362. border-radius: 8px;
  363. border: 1px solid rgba(255, 255, 255, 0.2);
  364. margin-bottom: 12px;
  365. background-color: rgba(39, 44, 48, 0.95);
  366. color: #e7e9ea;
  367. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  368. font-size: 13px;
  369. transition: border-color 0.2s;
  370. }
  371. #openrouter-api-key:focus,
  372. #user-instructions:focus {
  373. border-color: #1d9bf0;
  374. outline: none;
  375. }
  376.  
  377. #user-instructions {
  378. height: 120px;
  379. resize: vertical;
  380. }
  381.  
  382. /* Parameter controls */
  383. .parameter-row {
  384. display: flex;
  385. align-items: center;
  386. margin-bottom: 12px;
  387. gap: 8px;
  388. padding: 6px;
  389. border-radius: 8px;
  390. transition: background-color 0.2s;
  391. }
  392. .parameter-row:hover {
  393. background-color: rgba(255, 255, 255, 0.05);
  394. }
  395.  
  396. .parameter-label {
  397. flex: 1;
  398. font-size: 13px;
  399. color: #e7e9ea;
  400. }
  401.  
  402. .parameter-control {
  403. flex: 1.5;
  404. display: flex;
  405. align-items: center;
  406. gap: 8px;
  407. }
  408.  
  409. .parameter-value {
  410. min-width: 28px;
  411. text-align: center;
  412. background-color: rgba(255, 255, 255, 0.1);
  413. padding: 3px 5px;
  414. border-radius: 4px;
  415. font-size: 12px;
  416. }
  417.  
  418. .parameter-slider {
  419. flex: 1;
  420. -webkit-appearance: none;
  421. height: 4px;
  422. border-radius: 4px;
  423. background: rgba(255, 255, 255, 0.2);
  424. outline: none;
  425. cursor: pointer;
  426. }
  427.  
  428. .parameter-slider::-webkit-slider-thumb {
  429. -webkit-appearance: none;
  430. appearance: none;
  431. width: 14px;
  432. height: 14px;
  433. border-radius: 50%;
  434. background: #1d9bf0;
  435. cursor: pointer;
  436. transition: transform 0.1s;
  437. }
  438. .parameter-slider::-webkit-slider-thumb:hover {
  439. transform: scale(1.2);
  440. }
  441.  
  442. /* Section styles */
  443. .section-title {
  444. font-weight: bold;
  445. margin-top: 20px;
  446. margin-bottom: 8px;
  447. color: #e7e9ea;
  448. display: flex;
  449. align-items: center;
  450. gap: 6px;
  451. font-size: 14px;
  452. }
  453. .section-title:first-child {
  454. margin-top: 0;
  455. }
  456.  
  457. .section-description {
  458. font-size: 12px;
  459. margin-bottom: 8px;
  460. opacity: 0.8;
  461. line-height: 1.4;
  462. }
  463. /* Advanced options section */
  464. .advanced-options {
  465. margin-top: 5px;
  466. margin-bottom: 15px;
  467. border: 1px solid rgba(255, 255, 255, 0.1);
  468. border-radius: 8px;
  469. padding: 12px;
  470. background-color: rgba(255, 255, 255, 0.03);
  471. overflow: hidden;
  472. }
  473. .advanced-toggle {
  474. display: flex;
  475. justify-content: space-between;
  476. align-items: center;
  477. cursor: pointer;
  478. margin-bottom: 5px;
  479. }
  480. .advanced-toggle-title {
  481. font-weight: bold;
  482. font-size: 13px;
  483. color: #e7e9ea;
  484. }
  485. .advanced-toggle-icon {
  486. transition: transform 0.3s;
  487. }
  488. .advanced-toggle-icon.expanded {
  489. transform: rotate(180deg);
  490. }
  491. .advanced-content {
  492. max-height: 0;
  493. overflow: hidden;
  494. transition: max-height 0.3s ease-in-out;
  495. }
  496. .advanced-content.expanded {
  497. max-height: 300px;
  498. }
  499.  
  500. /* Handle list styling */
  501. .handle-list {
  502. margin-top: 10px;
  503. max-height: 120px;
  504. overflow-y: auto;
  505. border: 1px solid rgba(255, 255, 255, 0.1);
  506. border-radius: 8px;
  507. padding: 5px;
  508. }
  509.  
  510. .handle-item {
  511. display: flex;
  512. align-items: center;
  513. justify-content: space-between;
  514. padding: 6px 10px;
  515. border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  516. border-radius: 4px;
  517. transition: background-color 0.2s;
  518. }
  519. .handle-item:hover {
  520. background-color: rgba(255, 255, 255, 0.05);
  521. }
  522.  
  523. .handle-item:last-child {
  524. border-bottom: none;
  525. }
  526.  
  527. .handle-text {
  528. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  529. font-size: 12px;
  530. }
  531.  
  532. .remove-handle {
  533. background: none;
  534. border: none;
  535. color: #ff5c5c;
  536. cursor: pointer;
  537. font-size: 14px;
  538. padding: 0 3px;
  539. opacity: 0.7;
  540. transition: opacity 0.2s;
  541. }
  542. .remove-handle:hover {
  543. opacity: 1;
  544. }
  545.  
  546. .add-handle-btn {
  547. background-color: #1d9bf0;
  548. color: white;
  549. border: none;
  550. border-radius: 6px;
  551. padding: 7px 10px;
  552. cursor: pointer;
  553. font-weight: bold;
  554. font-size: 12px;
  555. margin-left: 5px;
  556. transition: background-color 0.2s;
  557. }
  558. .add-handle-btn:hover {
  559. background-color: #1a8cd8;
  560. }
  561.  
  562. /* Button styling */
  563. .settings-button {
  564. background-color: #1d9bf0;
  565. color: white;
  566. border: none;
  567. border-radius: 8px;
  568. padding: 10px 14px;
  569. cursor: pointer;
  570. font-weight: bold;
  571. transition: background-color 0.2s;
  572. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  573. margin-top: 8px;
  574. width: 100%;
  575. font-size: 13px;
  576. }
  577.  
  578. .settings-button:hover {
  579. background-color: #1a8cd8;
  580. }
  581.  
  582. .settings-button.secondary {
  583. background-color: rgba(255, 255, 255, 0.1);
  584. }
  585. .settings-button.secondary:hover {
  586. background-color: rgba(255, 255, 255, 0.15);
  587. }
  588.  
  589. .settings-button.danger {
  590. background-color: #ff5c5c;
  591. }
  592.  
  593. .settings-button.danger:hover {
  594. background-color: #e53935;
  595. }
  596. /* For smaller buttons that sit side by side */
  597. .button-row {
  598. display: flex;
  599. gap: 8px;
  600. margin-top: 10px;
  601. }
  602. .button-row .settings-button {
  603. margin-top: 0;
  604. }
  605. /* Stats display */
  606. .stats-container {
  607. background-color: rgba(255, 255, 255, 0.05);
  608. padding: 10px;
  609. border-radius: 8px;
  610. margin-bottom: 15px;
  611. }
  612. .stats-row {
  613. display: flex;
  614. justify-content: space-between;
  615. padding: 5px 0;
  616. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  617. }
  618. .stats-row:last-child {
  619. border-bottom: none;
  620. }
  621. .stats-label {
  622. font-size: 12px;
  623. opacity: 0.8;
  624. }
  625. .stats-value {
  626. font-weight: bold;
  627. }
  628.  
  629. /* Rating indicator shown on tweets */
  630. .score-indicator {
  631. position: absolute;
  632. top: 10px;
  633. right: 10.5%;
  634. background-color: rgba(22, 24, 28, 0.9);
  635. color: #e7e9ea;
  636. padding: 4px 10px;
  637. border-radius: 8px;
  638. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  639. font-size: 14px;
  640. font-weight: bold;
  641. z-index: 100;
  642. cursor: pointer;
  643. border: 1px solid rgba(255, 255, 255, 0.1);
  644. min-width: 20px;
  645. text-align: center;
  646. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  647. transition: transform 0.15s ease;
  648. }
  649. .score-indicator:hover {
  650. transform: scale(1.05);
  651. }
  652.  
  653. /* Refresh animation */
  654. .refreshing {
  655. animation: spin 1s infinite linear;
  656. }
  657.  
  658. @keyframes spin {
  659. 0% { transform: rotate(0deg); }
  660. 100% { transform: rotate(360deg); }
  661. }
  662.  
  663. /* The description box for ratings */
  664. .score-description {
  665. display: none;
  666. background-color: rgba(22, 24, 28, 0.95);
  667. color: #e7e9ea;
  668. padding: 16px 20px;
  669. border-radius: 12px;
  670. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  671. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  672. font-size: 16px;
  673. line-height: 1.5;
  674. z-index: 99999999;
  675. position: absolute;
  676. width: clamp(300px, 30vw, 500px);
  677. max-height: 60vh;
  678. overflow-y: auto;
  679. border: 1px solid rgba(255, 255, 255, 0.1);
  680. word-wrap: break-word;
  681. }
  682.  
  683. /* Rating status classes */
  684. .cached-rating {
  685. background-color: rgba(76, 175, 80, 0.9) !important;
  686. color: white !important;
  687. }
  688.  
  689. .blacklisted-rating {
  690. background-color: rgba(255, 193, 7, 0.9) !important;
  691. color: black !important;
  692. }
  693.  
  694. .pending-rating {
  695. background-color: rgba(255, 152, 0, 0.9) !important;
  696. color: white !important;
  697. }
  698.  
  699. .error-rating {
  700. background-color: rgba(244, 67, 54, 0.9) !important;
  701. color: white !important;
  702. }
  703.  
  704. /* Status indicator at bottom-right */
  705. #status-indicator {
  706. position: fixed;
  707. bottom: 20px;
  708. right: 20px;
  709. background-color: rgba(22, 24, 28, 0.95);
  710. color: #e7e9ea;
  711. padding: 10px 15px;
  712. border-radius: 8px;
  713. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  714. font-size: 12px;
  715. z-index: 9999;
  716. display: none;
  717. border: 1px solid rgba(255, 255, 255, 0.1);
  718. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
  719. transform: translateY(100px);
  720. transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  721. }
  722.  
  723. #status-indicator.active {
  724. display: block;
  725. transform: translateY(0);
  726. }
  727. /* Toggle switch styling */
  728. .toggle-switch {
  729. position: relative;
  730. display: inline-block;
  731. width: 36px;
  732. height: 20px;
  733. }
  734. .toggle-switch input {
  735. opacity: 0;
  736. width: 0;
  737. height: 0;
  738. }
  739. .toggle-slider {
  740. position: absolute;
  741. cursor: pointer;
  742. top: 0;
  743. left: 0;
  744. right: 0;
  745. bottom: 0;
  746. background-color: rgba(255, 255, 255, 0.2);
  747. transition: .3s;
  748. border-radius: 34px;
  749. }
  750. .toggle-slider:before {
  751. position: absolute;
  752. content: "";
  753. height: 16px;
  754. width: 16px;
  755. left: 2px;
  756. bottom: 2px;
  757. background-color: white;
  758. transition: .3s;
  759. border-radius: 50%;
  760. }
  761. input:checked + .toggle-slider {
  762. background-color: #1d9bf0;
  763. }
  764. input:checked + .toggle-slider:before {
  765. transform: translateX(16px);
  766. }
  767. .toggle-row {
  768. display: flex;
  769. align-items: center;
  770. justify-content: space-between;
  771. padding: 8px 10px;
  772. margin-bottom: 12px;
  773. background-color: rgba(255, 255, 255, 0.05);
  774. border-radius: 8px;
  775. transition: background-color 0.2s;
  776. }
  777. .toggle-row:hover {
  778. background-color: rgba(255, 255, 255, 0.08);
  779. }
  780. .toggle-label {
  781. font-size: 13px;
  782. color: #e7e9ea;
  783. }
  784.  
  785. /* Existing styles */
  786. /* Sort container styles */
  787. .sort-container {
  788. margin: 10px 0;
  789. display: flex;
  790. align-items: center;
  791. gap: 10px;
  792. }
  793. .sort-container label {
  794. font-size: 14px;
  795. color: var(--text-color);
  796. }
  797. .sort-container select {
  798. padding: 5px 10px;
  799. border-radius: 4px;
  800. border: 1px solid rgba(255, 255, 255, 0.2);
  801. background-color: rgba(39, 44, 48, 0.95);
  802. color: #e7e9ea;
  803. font-size: 14px;
  804. cursor: pointer;
  805. }
  806. .sort-container select:hover {
  807. border-color: #1d9bf0;
  808. }
  809. .sort-container select:focus {
  810. outline: none;
  811. border-color: #1d9bf0;
  812. box-shadow: 0 0 0 2px rgba(29, 155, 240, 0.2);
  813. }
  814. /* Dropdown option styling */
  815. .sort-container select option {
  816. background-color: rgba(39, 44, 48, 0.95);
  817. color: #e7e9ea;
  818. }
  819. </style>
  820. <div id="tweetfilter-root-container">
  821. <button id="filter-toggle" class="toggle-button" style="display: none;">Filter Slider</button>
  822. <div id="tweet-filter-container">
  823. <button class="close-button" data-action="close-filter">×</button>
  824. <label for="tweet-filter-slider">SlopScore:</label>
  825. <input type="range" id="tweet-filter-slider" min="1" max="10" step="1">
  826. <span id="tweet-filter-value">5</span>
  827. </div>
  828.  
  829. <button id="settings-toggle" class="toggle-button">
  830. <span style="font-size: 14px;">⚙️</span> Settings
  831. </button>
  832. <div id="settings-container" class="hidden">
  833. <div class="settings-header">
  834. <div class="settings-title">Twitter De-Sloppifier</div>
  835. <button class="close-button" data-action="close-settings">×</button>
  836. </div>
  837. <div class="settings-content">
  838. <div class="tab-navigation">
  839. <button class="tab-button active" data-tab="general">General</button>
  840. <button class="tab-button" data-tab="models">Models</button>
  841. <button class="tab-button" data-tab="instructions">Instructions</button>
  842. </div>
  843. <div id="general-tab" class="tab-content active">
  844. <div class="section-title"><span style="font-size: 14px;">🔑</span> OpenRouter API Key <a href="https://openrouter.ai/" target="_blank">Get one here</a></div>
  845. <input id="openrouter-api-key" placeholder="Enter your OpenRouter API key">
  846. <button class="settings-button" data-action="save-api-key">Save API Key</button>
  847. <div class="section-title" style="margin-top: 20px;"><span style="font-size: 14px;">🗄️</span> Cache Statistics</div>
  848. <div class="stats-container">
  849. <div class="stats-row">
  850. <div class="stats-label">Cached Tweet Ratings</div>
  851. <div class="stats-value" id="cached-ratings-count">0</div>
  852. </div>
  853. <div class="stats-row">
  854. <div class="stats-label">Whitelisted Handles</div>
  855. <div class="stats-value" id="whitelisted-handles-count">0</div>
  856. </div>
  857. </div>
  858. <button id="clear-cache" class="settings-button danger" data-action="clear-cache">Clear Rating Cache</button>
  859. <div class="section-title" style="margin-top: 20px;">
  860. <span style="font-size: 14px;">💾</span> Backup &amp; Restore
  861. </div>
  862. <div class="section-description">
  863. Export your settings and cached ratings to a file for backup, or import previously saved settings.
  864. </div>
  865. <div class="button-row">
  866. <button class="settings-button secondary" data-action="export-settings">Export Settings</button>
  867. <button class="settings-button secondary" data-action="import-settings">Import Settings</button>
  868. </div>
  869. <button class="settings-button danger" style="margin-top: 15px;" data-action="reset-settings">Reset to Defaults</button>
  870. <div id="version-info" style="margin-top: 20px; font-size: 11px; opacity: 0.6; text-align: center;">Twitter De-Sloppifier v?.?</div>
  871. </div>
  872. <div id="models-tab" class="tab-content">
  873. <div class="section-title">
  874. <span style="font-size: 14px;">🧠</span> Tweet Rating Model
  875. </div>
  876. <div class="section-description">
  877. Hint: If you want to rate tweets with images, you need to select an image model.
  878. </div>
  879. <div class="sort-container">
  880. <label for="model-sort-order">Sort models by: </label>
  881. <select id="model-sort-order" data-setting="modelSortOrder">
  882. <option value="price-low-to-high">Price (Low to High)</option>
  883. <option value="price-high-to-low">Price (High to Low)</option>
  884. <option value="throughput-high-to-low">Throughput (High to Low)</option>
  885. <option value="throughput-low-to-high">Throughput (Low to High)</option>
  886. <option value="latency-low-to-high">Latency (Low to High)</option>
  887. <option value="latency-high-to-low">Latency (High to Low)</option>
  888. </select>
  889. </div>
  890. <div class="select-container" id="model-select-container">
  891. </div>
  892. <div class="advanced-options" id="rating-advanced-options">
  893. <div class="advanced-toggle" data-toggle="rating-advanced-content">
  894. <div class="advanced-toggle-title">Advanced Options</div>
  895. <div class="advanced-toggle-icon">▼</div>
  896. </div>
  897. <div class="advanced-content" id="rating-advanced-content">
  898. <div class="parameter-row" data-param-name="modelTemperature">
  899. <div class="parameter-label" title="How random the model responses should be (0.0-1.0)">Temperature</div>
  900. <div class="parameter-control">
  901. <input type="range" class="parameter-slider" min="0" max="1" step="0.1">
  902. <input type="number" class="parameter-value" min="0" max="1" step="0.1" style="width: 60px;">
  903. </div>
  904. </div>
  905. <div class="parameter-row" data-param-name="modelTopP">
  906. <div class="parameter-label" title="Nucleus sampling parameter (0.0-1.0)">Top-p</div>
  907. <div class="parameter-control">
  908. <input type="range" class="parameter-slider" min="0" max="1" step="0.1">
  909. <input type="number" class="parameter-value" min="0" max="1" step="0.1" style="width: 60px;">
  910. </div>
  911. </div>
  912. <div class="parameter-row" data-param-name="maxTokens">
  913. <div class="parameter-label" title="Maximum number of tokens for the response (0 means no limit)">Max Tokens</div>
  914. <div class="parameter-control">
  915. <input type="range" class="parameter-slider" min="0" max="2000" step="100">
  916. <input type="number" class="parameter-value" min="0" max="2000" step="100" style="width: 60px;">
  917. </div>
  918. </div>
  919. </div>
  920. </div>
  921. <div class="section-title" style="margin-top: 25px;"><span style="font-size: 14px;">🖼️</span> Image Processing Model</div>
  922. <div class="section-description">This model generates <strong>text descriptions</strong> of images, which are then sent to the rating model above. If you've selected an image-capable model (🖼️) as your main rating model above, you can disable this to process images directly.</div>
  923. <div class="toggle-row">
  924. <div class="toggle-label">Enable Image Descriptions</div>
  925. <label class="toggle-switch">
  926. <input type="checkbox" data-setting="enableImageDescriptions">
  927. <span class="toggle-slider"></span>
  928. </label>
  929. </div>
  930. <div id="image-model-container" style="display: none;">
  931. <div class="section-description">Select a model with vision capabilities to describe images in tweets.</div>
  932. <div class="select-container" id="image-model-select-container">
  933. </div>
  934. <div class="advanced-options" id="image-advanced-options">
  935. <div class="advanced-toggle" data-toggle="image-advanced-content">
  936. <div class="advanced-toggle-title">Advanced Options</div>
  937. <div class="advanced-toggle-icon">▼</div>
  938. </div>
  939. <div class="advanced-content" id="image-advanced-content">
  940. <div class="parameter-row" data-param-name="imageModelTemperature">
  941. <div class="parameter-label" title="Randomness for image descriptions (0.0-1.0)">Temperature</div>
  942. <div class="parameter-control">
  943. <input type="range" class="parameter-slider" min="0" max="1" step="0.1">
  944. <input type="number" class="parameter-value" min="0" max="1" step="0.1" style="width: 60px;">
  945. </div>
  946. </div>
  947. <div class="parameter-row" data-param-name="imageModelTopP">
  948. <div class="parameter-label" title="Nucleus sampling for image model (0.0-1.0)">Top-p</div>
  949. <div class="parameter-control">
  950. <input type="range" class="parameter-slider" min="0" max="1" step="0.1">
  951. <input type="number" class="parameter-value" min="0" max="1" step="0.1" style="width: 60px;">
  952. </div>
  953. </div>
  954. </div>
  955. </div>
  956. </div>
  957. </div>
  958. <div id="instructions-tab" class="tab-content">
  959. <div class="section-title">Custom Tweet Rating Instructions</div>
  960. <div class="section-description">Add custom instructions for how the model should score tweets:</div>
  961. <textarea id="user-instructions" placeholder="Examples:
  962. - Give high scores to tweets about technology
  963. - Penalize clickbait-style tweets
  964. - Rate educational content higher" data-setting="userDefinedInstructions" value=""></textarea>
  965. <button class="settings-button" data-action="save-instructions">Save Instructions</button>
  966. <div class="section-title" style="margin-top: 20px;">Auto-Rate Handles as 10/10</div>
  967. <div class="section-description">Add Twitter handles to automatically rate as 10/10:</div>
  968. <div style="display: flex; align-items: center; gap: 5px;">
  969. <input id="handle-input" type="text" placeholder="Twitter handle (without @)">
  970. <button class="add-handle-btn" data-action="add-handle">Add</button>
  971. </div>
  972. <div class="handle-list" id="handle-list">
  973. </div>
  974. </div>
  975. </div>
  976. <div id="status-indicator" class=""></div>
  977. </div>
  978. </div>`;
  979.  
  980. // Embedded style.css
  981. const STYLE = `/*
  982. Modern X-Inspired Styles - Enhanced
  983. ---------------------------------
  984. */
  985.  
  986. /* Main tweet filter container */
  987. #tweet-filter-container {
  988. position: fixed;
  989. top: 70px;
  990. right: 15px;
  991. background-color: rgba(22, 24, 28, 0.95);
  992. color: #e7e9ea;
  993. padding: 10px 12px;
  994. border-radius: 12px;
  995. z-index: 9999;
  996. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  997. font-size: 13px;
  998. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
  999. display: flex;
  1000. align-items: center;
  1001. gap: 10px;
  1002. border: 1px solid rgba(255, 255, 255, 0.1);
  1003. }
  1004.  
  1005. /* Close button styles */
  1006. .close-button {
  1007. background: none;
  1008. border: none;
  1009. color: #e7e9ea;
  1010. font-size: 16px;
  1011. cursor: pointer;
  1012. padding: 0;
  1013. width: 28px;
  1014. height: 28px;
  1015. display: flex;
  1016. align-items: center;
  1017. justify-content: center;
  1018. opacity: 0.8;
  1019. transition: opacity 0.2s;
  1020. border-radius: 50%;
  1021. }
  1022.  
  1023. .close-button:hover {
  1024. opacity: 1;
  1025. background-color: rgba(255, 255, 255, 0.1);
  1026. }
  1027.  
  1028. /* Hidden state */
  1029. .hidden {
  1030. display: none !important;
  1031. }
  1032.  
  1033. /* Show/hide button */
  1034. .toggle-button {
  1035. position: fixed;
  1036. right: 15px;
  1037. background-color: rgba(22, 24, 28, 0.95);
  1038. color: #e7e9ea;
  1039. padding: 8px 12px;
  1040. border-radius: 8px;
  1041. cursor: pointer;
  1042. font-size: 12px;
  1043. z-index: 9999;
  1044. border: 1px solid rgba(255, 255, 255, 0.1);
  1045. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
  1046. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  1047. display: flex;
  1048. align-items: center;
  1049. gap: 6px;
  1050. transition: all 0.2s ease;
  1051. }
  1052. .toggle-button:hover {
  1053. background-color: rgba(29, 155, 240, 0.2);
  1054. }
  1055.  
  1056. #filter-toggle {
  1057. top: 70px;
  1058. }
  1059.  
  1060. #settings-toggle {
  1061. top: 120px;
  1062. }
  1063.  
  1064. #tweet-filter-container label {
  1065. margin: 0;
  1066. font-weight: bold;
  1067. }
  1068.  
  1069. #tweet-filter-slider {
  1070. cursor: pointer;
  1071. width: 120px;
  1072. vertical-align: middle;
  1073. accent-color: #1d9bf0;
  1074. }
  1075.  
  1076. #tweet-filter-value {
  1077. min-width: 20px;
  1078. text-align: center;
  1079. font-weight: bold;
  1080. background-color: rgba(255, 255, 255, 0.1);
  1081. padding: 2px 5px;
  1082. border-radius: 4px;
  1083. }
  1084.  
  1085. /* Settings UI with Tabs */
  1086. #settings-container {
  1087. position: fixed;
  1088. top: 70px;
  1089. right: 15px;
  1090. background-color: rgba(22, 24, 28, 0.95);
  1091. color: #e7e9ea;
  1092. padding: 0; /* Remove padding to accommodate sticky header */
  1093. border-radius: 16px;
  1094. z-index: 9999;
  1095. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  1096. font-size: 13px;
  1097. box-shadow: 0 2px 18px rgba(0, 0, 0, 0.6);
  1098. display: flex;
  1099. flex-direction: column;
  1100. width: 380px;
  1101. max-height: 85vh;
  1102. overflow: hidden; /* Hide overflow to make the sticky header work properly */
  1103. border: 1px solid rgba(255, 255, 255, 0.1);
  1104. line-height: 1.3;
  1105. transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  1106. transform-origin: top right;
  1107. }
  1108. #settings-container.hidden {
  1109. opacity: 0;
  1110. transform: scale(0.9);
  1111. pointer-events: none;
  1112. }
  1113. /* Header section */
  1114. .settings-header {
  1115. padding: 12px 15px;
  1116. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  1117. display: flex;
  1118. justify-content: space-between;
  1119. align-items: center;
  1120. position: sticky;
  1121. top: 0;
  1122. background-color: rgba(22, 24, 28, 0.98);
  1123. z-index: 20;
  1124. border-radius: 16px 16px 0 0;
  1125. }
  1126. .settings-title {
  1127. font-weight: bold;
  1128. font-size: 16px;
  1129. }
  1130. /* Content area with scrolling */
  1131. .settings-content {
  1132. overflow-y: auto;
  1133. max-height: calc(85vh - 110px); /* Account for header and tabs */
  1134. padding: 0;
  1135. }
  1136. /* Scrollbar styling for settings container */
  1137. .settings-content::-webkit-scrollbar {
  1138. width: 6px;
  1139. }
  1140.  
  1141. .settings-content::-webkit-scrollbar-track {
  1142. background: rgba(255, 255, 255, 0.05);
  1143. border-radius: 3px;
  1144. }
  1145.  
  1146. .settings-content::-webkit-scrollbar-thumb {
  1147. background: rgba(255, 255, 255, 0.2);
  1148. border-radius: 3px;
  1149. }
  1150.  
  1151. .settings-content::-webkit-scrollbar-thumb:hover {
  1152. background: rgba(255, 255, 255, 0.3);
  1153. }
  1154.  
  1155. /* Tab Navigation */
  1156. .tab-navigation {
  1157. display: flex;
  1158. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  1159. position: sticky;
  1160. top: 0;
  1161. background-color: rgba(22, 24, 28, 0.98);
  1162. z-index: 10;
  1163. padding: 10px 15px;
  1164. gap: 8px;
  1165. }
  1166.  
  1167. .tab-button {
  1168. padding: 6px 10px;
  1169. background: none;
  1170. border: none;
  1171. color: #e7e9ea;
  1172. font-weight: bold;
  1173. cursor: pointer;
  1174. border-radius: 8px;
  1175. transition: all 0.2s ease;
  1176. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  1177. font-size: 13px;
  1178. flex: 1;
  1179. text-align: center;
  1180. }
  1181.  
  1182. .tab-button:hover {
  1183. background-color: rgba(255, 255, 255, 0.1);
  1184. }
  1185.  
  1186. .tab-button.active {
  1187. color: #1d9bf0;
  1188. background-color: rgba(29, 155, 240, 0.1);
  1189. border-bottom: 2px solid #1d9bf0;
  1190. }
  1191.  
  1192. /* Tab Content */
  1193. .tab-content {
  1194. display: none;
  1195. animation: fadeIn 0.3s ease;
  1196. padding: 15px;
  1197. }
  1198. @keyframes fadeIn {
  1199. from { opacity: 0; }
  1200. to { opacity: 1; }
  1201. }
  1202.  
  1203. .tab-content.active {
  1204. display: block;
  1205. }
  1206.  
  1207. /* Enhanced dropdowns */
  1208. .select-container {
  1209. position: relative;
  1210. margin-bottom: 15px;
  1211. }
  1212. .select-container .search-field {
  1213. position: sticky;
  1214. top: 0;
  1215. background-color: rgba(39, 44, 48, 0.95);
  1216. padding: 8px;
  1217. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  1218. z-index: 1;
  1219. }
  1220. .select-container .search-input {
  1221. width: 100%;
  1222. padding: 8px 10px;
  1223. border-radius: 8px;
  1224. border: 1px solid rgba(255, 255, 255, 0.2);
  1225. background-color: rgba(39, 44, 48, 0.9);
  1226. color: #e7e9ea;
  1227. font-size: 12px;
  1228. transition: border-color 0.2s;
  1229. }
  1230. .select-container .search-input:focus {
  1231. border-color: #1d9bf0;
  1232. outline: none;
  1233. }
  1234. .custom-select {
  1235. position: relative;
  1236. display: inline-block;
  1237. width: 100%;
  1238. }
  1239. .select-selected {
  1240. background-color: rgba(39, 44, 48, 0.95);
  1241. color: #e7e9ea;
  1242. padding: 10px 12px;
  1243. border: 1px solid rgba(255, 255, 255, 0.2);
  1244. border-radius: 8px;
  1245. cursor: pointer;
  1246. user-select: none;
  1247. display: flex;
  1248. justify-content: space-between;
  1249. align-items: center;
  1250. font-size: 13px;
  1251. transition: border-color 0.2s;
  1252. }
  1253. .select-selected:hover {
  1254. border-color: rgba(255, 255, 255, 0.4);
  1255. }
  1256. .select-selected:after {
  1257. content: "";
  1258. width: 8px;
  1259. height: 8px;
  1260. border: 2px solid #e7e9ea;
  1261. border-width: 0 2px 2px 0;
  1262. display: inline-block;
  1263. transform: rotate(45deg);
  1264. margin-left: 10px;
  1265. transition: transform 0.2s;
  1266. }
  1267. .select-selected.select-arrow-active:after {
  1268. transform: rotate(-135deg);
  1269. }
  1270. .select-items {
  1271. position: absolute;
  1272. background-color: rgba(39, 44, 48, 0.98);
  1273. top: 100%;
  1274. left: 0;
  1275. right: 0;
  1276. z-index: 99;
  1277. max-height: 300px;
  1278. overflow-y: auto;
  1279. border: 1px solid rgba(255, 255, 255, 0.2);
  1280. border-radius: 8px;
  1281. margin-top: 5px;
  1282. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
  1283. display: none;
  1284. }
  1285. .select-items div {
  1286. color: #e7e9ea;
  1287. padding: 10px 12px;
  1288. cursor: pointer;
  1289. user-select: none;
  1290. transition: background-color 0.2s;
  1291. border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  1292. }
  1293. .select-items div:hover {
  1294. background-color: rgba(29, 155, 240, 0.1);
  1295. }
  1296. .select-items div.same-as-selected {
  1297. background-color: rgba(29, 155, 240, 0.2);
  1298. }
  1299. /* Scrollbar for select items */
  1300. .select-items::-webkit-scrollbar {
  1301. width: 6px;
  1302. }
  1303. .select-items::-webkit-scrollbar-track {
  1304. background: rgba(255, 255, 255, 0.05);
  1305. }
  1306. .select-items::-webkit-scrollbar-thumb {
  1307. background: rgba(255, 255, 255, 0.2);
  1308. border-radius: 3px;
  1309. }
  1310. .select-items::-webkit-scrollbar-thumb:hover {
  1311. background: rgba(255, 255, 255, 0.3);
  1312. }
  1313. /* Form elements */
  1314. #openrouter-api-key,
  1315. #user-instructions {
  1316. width: 100%;
  1317. padding: 10px 12px;
  1318. border-radius: 8px;
  1319. border: 1px solid rgba(255, 255, 255, 0.2);
  1320. margin-bottom: 12px;
  1321. background-color: rgba(39, 44, 48, 0.95);
  1322. color: #e7e9ea;
  1323. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  1324. font-size: 13px;
  1325. transition: border-color 0.2s;
  1326. }
  1327. #openrouter-api-key:focus,
  1328. #user-instructions:focus {
  1329. border-color: #1d9bf0;
  1330. outline: none;
  1331. }
  1332.  
  1333. #user-instructions {
  1334. height: 120px;
  1335. resize: vertical;
  1336. }
  1337.  
  1338. /* Parameter controls */
  1339. .parameter-row {
  1340. display: flex;
  1341. align-items: center;
  1342. margin-bottom: 12px;
  1343. gap: 8px;
  1344. padding: 6px;
  1345. border-radius: 8px;
  1346. transition: background-color 0.2s;
  1347. }
  1348. .parameter-row:hover {
  1349. background-color: rgba(255, 255, 255, 0.05);
  1350. }
  1351.  
  1352. .parameter-label {
  1353. flex: 1;
  1354. font-size: 13px;
  1355. color: #e7e9ea;
  1356. }
  1357.  
  1358. .parameter-control {
  1359. flex: 1.5;
  1360. display: flex;
  1361. align-items: center;
  1362. gap: 8px;
  1363. }
  1364.  
  1365. .parameter-value {
  1366. min-width: 28px;
  1367. text-align: center;
  1368. background-color: rgba(255, 255, 255, 0.1);
  1369. padding: 3px 5px;
  1370. border-radius: 4px;
  1371. font-size: 12px;
  1372. }
  1373.  
  1374. .parameter-slider {
  1375. flex: 1;
  1376. -webkit-appearance: none;
  1377. height: 4px;
  1378. border-radius: 4px;
  1379. background: rgba(255, 255, 255, 0.2);
  1380. outline: none;
  1381. cursor: pointer;
  1382. }
  1383.  
  1384. .parameter-slider::-webkit-slider-thumb {
  1385. -webkit-appearance: none;
  1386. appearance: none;
  1387. width: 14px;
  1388. height: 14px;
  1389. border-radius: 50%;
  1390. background: #1d9bf0;
  1391. cursor: pointer;
  1392. transition: transform 0.1s;
  1393. }
  1394. .parameter-slider::-webkit-slider-thumb:hover {
  1395. transform: scale(1.2);
  1396. }
  1397.  
  1398. /* Section styles */
  1399. .section-title {
  1400. font-weight: bold;
  1401. margin-top: 20px;
  1402. margin-bottom: 8px;
  1403. color: #e7e9ea;
  1404. display: flex;
  1405. align-items: center;
  1406. gap: 6px;
  1407. font-size: 14px;
  1408. }
  1409. .section-title:first-child {
  1410. margin-top: 0;
  1411. }
  1412.  
  1413. .section-description {
  1414. font-size: 12px;
  1415. margin-bottom: 8px;
  1416. opacity: 0.8;
  1417. line-height: 1.4;
  1418. }
  1419. /* Advanced options section */
  1420. .advanced-options {
  1421. margin-top: 5px;
  1422. margin-bottom: 15px;
  1423. border: 1px solid rgba(255, 255, 255, 0.1);
  1424. border-radius: 8px;
  1425. padding: 12px;
  1426. background-color: rgba(255, 255, 255, 0.03);
  1427. overflow: hidden;
  1428. }
  1429. .advanced-toggle {
  1430. display: flex;
  1431. justify-content: space-between;
  1432. align-items: center;
  1433. cursor: pointer;
  1434. margin-bottom: 5px;
  1435. }
  1436. .advanced-toggle-title {
  1437. font-weight: bold;
  1438. font-size: 13px;
  1439. color: #e7e9ea;
  1440. }
  1441. .advanced-toggle-icon {
  1442. transition: transform 0.3s;
  1443. }
  1444. .advanced-toggle-icon.expanded {
  1445. transform: rotate(180deg);
  1446. }
  1447. .advanced-content {
  1448. max-height: 0;
  1449. overflow: hidden;
  1450. transition: max-height 0.3s ease-in-out;
  1451. }
  1452. .advanced-content.expanded {
  1453. max-height: 300px;
  1454. }
  1455.  
  1456. /* Handle list styling */
  1457. .handle-list {
  1458. margin-top: 10px;
  1459. max-height: 120px;
  1460. overflow-y: auto;
  1461. border: 1px solid rgba(255, 255, 255, 0.1);
  1462. border-radius: 8px;
  1463. padding: 5px;
  1464. }
  1465.  
  1466. .handle-item {
  1467. display: flex;
  1468. align-items: center;
  1469. justify-content: space-between;
  1470. padding: 6px 10px;
  1471. border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  1472. border-radius: 4px;
  1473. transition: background-color 0.2s;
  1474. }
  1475. .handle-item:hover {
  1476. background-color: rgba(255, 255, 255, 0.05);
  1477. }
  1478.  
  1479. .handle-item:last-child {
  1480. border-bottom: none;
  1481. }
  1482.  
  1483. .handle-text {
  1484. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  1485. font-size: 12px;
  1486. }
  1487.  
  1488. .remove-handle {
  1489. background: none;
  1490. border: none;
  1491. color: #ff5c5c;
  1492. cursor: pointer;
  1493. font-size: 14px;
  1494. padding: 0 3px;
  1495. opacity: 0.7;
  1496. transition: opacity 0.2s;
  1497. }
  1498. .remove-handle:hover {
  1499. opacity: 1;
  1500. }
  1501.  
  1502. .add-handle-btn {
  1503. background-color: #1d9bf0;
  1504. color: white;
  1505. border: none;
  1506. border-radius: 6px;
  1507. padding: 7px 10px;
  1508. cursor: pointer;
  1509. font-weight: bold;
  1510. font-size: 12px;
  1511. margin-left: 5px;
  1512. transition: background-color 0.2s;
  1513. }
  1514. .add-handle-btn:hover {
  1515. background-color: #1a8cd8;
  1516. }
  1517.  
  1518. /* Button styling */
  1519. .settings-button {
  1520. background-color: #1d9bf0;
  1521. color: white;
  1522. border: none;
  1523. border-radius: 8px;
  1524. padding: 10px 14px;
  1525. cursor: pointer;
  1526. font-weight: bold;
  1527. transition: background-color 0.2s;
  1528. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  1529. margin-top: 8px;
  1530. width: 100%;
  1531. font-size: 13px;
  1532. }
  1533.  
  1534. .settings-button:hover {
  1535. background-color: #1a8cd8;
  1536. }
  1537.  
  1538. .settings-button.secondary {
  1539. background-color: rgba(255, 255, 255, 0.1);
  1540. }
  1541. .settings-button.secondary:hover {
  1542. background-color: rgba(255, 255, 255, 0.15);
  1543. }
  1544.  
  1545. .settings-button.danger {
  1546. background-color: #ff5c5c;
  1547. }
  1548.  
  1549. .settings-button.danger:hover {
  1550. background-color: #e53935;
  1551. }
  1552. /* For smaller buttons that sit side by side */
  1553. .button-row {
  1554. display: flex;
  1555. gap: 8px;
  1556. margin-top: 10px;
  1557. }
  1558. .button-row .settings-button {
  1559. margin-top: 0;
  1560. }
  1561. /* Stats display */
  1562. .stats-container {
  1563. background-color: rgba(255, 255, 255, 0.05);
  1564. padding: 10px;
  1565. border-radius: 8px;
  1566. margin-bottom: 15px;
  1567. }
  1568. .stats-row {
  1569. display: flex;
  1570. justify-content: space-between;
  1571. padding: 5px 0;
  1572. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  1573. }
  1574. .stats-row:last-child {
  1575. border-bottom: none;
  1576. }
  1577. .stats-label {
  1578. font-size: 12px;
  1579. opacity: 0.8;
  1580. }
  1581. .stats-value {
  1582. font-weight: bold;
  1583. }
  1584.  
  1585. /* Rating indicator shown on tweets */
  1586. .score-indicator {
  1587. position: absolute;
  1588. top: 10px;
  1589. right: 10.5%;
  1590. background-color: rgba(22, 24, 28, 0.9);
  1591. color: #e7e9ea;
  1592. padding: 4px 10px;
  1593. border-radius: 8px;
  1594. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  1595. font-size: 14px;
  1596. font-weight: bold;
  1597. z-index: 100;
  1598. cursor: pointer;
  1599. border: 1px solid rgba(255, 255, 255, 0.1);
  1600. min-width: 20px;
  1601. text-align: center;
  1602. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  1603. transition: transform 0.15s ease;
  1604. }
  1605. .score-indicator:hover {
  1606. transform: scale(1.05);
  1607. }
  1608.  
  1609. /* Refresh animation */
  1610. .refreshing {
  1611. animation: spin 1s infinite linear;
  1612. }
  1613.  
  1614. @keyframes spin {
  1615. 0% { transform: rotate(0deg); }
  1616. 100% { transform: rotate(360deg); }
  1617. }
  1618.  
  1619. /* The description box for ratings */
  1620. .score-description {
  1621. display: none;
  1622. background-color: rgba(22, 24, 28, 0.95);
  1623. color: #e7e9ea;
  1624. padding: 16px 20px;
  1625. border-radius: 12px;
  1626. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  1627. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  1628. font-size: 16px;
  1629. line-height: 1.5;
  1630. z-index: 99999999;
  1631. position: absolute;
  1632. width: clamp(300px, 30vw, 500px);
  1633. max-height: 60vh;
  1634. overflow-y: auto;
  1635. border: 1px solid rgba(255, 255, 255, 0.1);
  1636. word-wrap: break-word;
  1637. }
  1638.  
  1639. /* Rating status classes */
  1640. .cached-rating {
  1641. background-color: rgba(76, 175, 80, 0.9) !important;
  1642. color: white !important;
  1643. }
  1644.  
  1645. .blacklisted-rating {
  1646. background-color: rgba(255, 193, 7, 0.9) !important;
  1647. color: black !important;
  1648. }
  1649.  
  1650. .pending-rating {
  1651. background-color: rgba(255, 152, 0, 0.9) !important;
  1652. color: white !important;
  1653. }
  1654.  
  1655. .error-rating {
  1656. background-color: rgba(244, 67, 54, 0.9) !important;
  1657. color: white !important;
  1658. }
  1659.  
  1660. /* Status indicator at bottom-right */
  1661. #status-indicator {
  1662. position: fixed;
  1663. bottom: 20px;
  1664. right: 20px;
  1665. background-color: rgba(22, 24, 28, 0.95);
  1666. color: #e7e9ea;
  1667. padding: 10px 15px;
  1668. border-radius: 8px;
  1669. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  1670. font-size: 12px;
  1671. z-index: 9999;
  1672. display: none;
  1673. border: 1px solid rgba(255, 255, 255, 0.1);
  1674. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
  1675. transform: translateY(100px);
  1676. transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  1677. }
  1678.  
  1679. #status-indicator.active {
  1680. display: block;
  1681. transform: translateY(0);
  1682. }
  1683. /* Toggle switch styling */
  1684. .toggle-switch {
  1685. position: relative;
  1686. display: inline-block;
  1687. width: 36px;
  1688. height: 20px;
  1689. }
  1690. .toggle-switch input {
  1691. opacity: 0;
  1692. width: 0;
  1693. height: 0;
  1694. }
  1695. .toggle-slider {
  1696. position: absolute;
  1697. cursor: pointer;
  1698. top: 0;
  1699. left: 0;
  1700. right: 0;
  1701. bottom: 0;
  1702. background-color: rgba(255, 255, 255, 0.2);
  1703. transition: .3s;
  1704. border-radius: 34px;
  1705. }
  1706. .toggle-slider:before {
  1707. position: absolute;
  1708. content: "";
  1709. height: 16px;
  1710. width: 16px;
  1711. left: 2px;
  1712. bottom: 2px;
  1713. background-color: white;
  1714. transition: .3s;
  1715. border-radius: 50%;
  1716. }
  1717. input:checked + .toggle-slider {
  1718. background-color: #1d9bf0;
  1719. }
  1720. input:checked + .toggle-slider:before {
  1721. transform: translateX(16px);
  1722. }
  1723. .toggle-row {
  1724. display: flex;
  1725. align-items: center;
  1726. justify-content: space-between;
  1727. padding: 8px 10px;
  1728. margin-bottom: 12px;
  1729. background-color: rgba(255, 255, 255, 0.05);
  1730. border-radius: 8px;
  1731. transition: background-color 0.2s;
  1732. }
  1733. .toggle-row:hover {
  1734. background-color: rgba(255, 255, 255, 0.08);
  1735. }
  1736. .toggle-label {
  1737. font-size: 13px;
  1738. color: #e7e9ea;
  1739. }
  1740.  
  1741. /* Existing styles */
  1742. /* Sort container styles */
  1743. .sort-container {
  1744. margin: 10px 0;
  1745. display: flex;
  1746. align-items: center;
  1747. gap: 10px;
  1748. }
  1749. .sort-container label {
  1750. font-size: 14px;
  1751. color: var(--text-color);
  1752. }
  1753. .sort-container select {
  1754. padding: 5px 10px;
  1755. border-radius: 4px;
  1756. border: 1px solid rgba(255, 255, 255, 0.2);
  1757. background-color: rgba(39, 44, 48, 0.95);
  1758. color: #e7e9ea;
  1759. font-size: 14px;
  1760. cursor: pointer;
  1761. }
  1762. .sort-container select:hover {
  1763. border-color: #1d9bf0;
  1764. }
  1765. .sort-container select:focus {
  1766. outline: none;
  1767. border-color: #1d9bf0;
  1768. box-shadow: 0 0 0 2px rgba(29, 155, 240, 0.2);
  1769. }
  1770. /* Dropdown option styling */
  1771. .sort-container select option {
  1772. background-color: rgba(39, 44, 48, 0.95);
  1773. color: #e7e9ea;
  1774. }`;
  1775.  
  1776. // Apply CSS
  1777. GM_addStyle(STYLE);
  1778.  
  1779. // Set menu HTML
  1780. GM_setValue('menuHTML', MENU);
  1781.  
  1782. // ----- twitter-desloppifier.js -----
  1783. (function () {
  1784. 'use strict';
  1785. console.log("X/Twitter Tweet De-Sloppification Activated (v1.3 - Enhanced)");
  1786. // Load CSS stylesheet
  1787. //const css = GM_getResourceText('STYLESHEET');
  1788. let menuhtml = GM_getResourceText("MENU_HTML");
  1789. GM_setValue('menuHTML', menuhtml);
  1790. let firstRun = GM_getValue('firstRun', true);
  1791. //GM_addStyle(css);
  1792.  
  1793. // ----- Initialization -----
  1794. /**
  1795. * Initializes the observer on the main content area, adds the UI elements,
  1796. * starts processing visible tweets, and sets up periodic checks.
  1797. */
  1798. function initializeObserver() {
  1799. const target = document.querySelector('main') || document.querySelector('div[data-testid="primaryColumn"]');
  1800. if (target) {
  1801. observedTargetNode = target;
  1802. console.log("X/Twitter Tweet De-Sloppification: Target node found. Observing...");
  1803. initialiseUI();
  1804. if (firstRun){
  1805. resetSettings(true);
  1806. GM_setValue('firstRun', false);
  1807. }
  1808. // If no API key is found, prompt the user
  1809. const apiKey = GM_getValue('openrouter-api-key', '');
  1810. if (!apiKey) {
  1811. apiKey = prompt("<TweetFilter AI>\nPlease enter your OpenRouter API key. You can get one at https://openrouter.ai/");
  1812. if (apiKey) {
  1813. GM_setValue('openrouter-api-key', apiKey);
  1814. }
  1815. showStatus("No API key found. Please enter your OpenRouter API key.");
  1816. } else {
  1817. showStatus(`Loaded ${Object.keys(tweetIDRatingCache).length} cached ratings. Starting to rate visible tweets...`);
  1818. fetchAvailableModels();
  1819. }
  1820. // Process all currently visible tweets
  1821. observedTargetNode.querySelectorAll(TWEET_ARTICLE_SELECTOR).forEach(scheduleTweetProcessing);
  1822. const observer = new MutationObserver(handleMutations);
  1823. observer.observe(observedTargetNode, { childList: true, subtree: true });
  1824. // Periodically ensure all tweets have been processed
  1825. setInterval(ensureAllTweetsRated, 3000);
  1826. window.addEventListener('beforeunload', () => {
  1827. saveTweetRatings();
  1828. observer.disconnect();
  1829. const sliderUI = document.getElementById('tweet-filter-container');
  1830. if (sliderUI) sliderUI.remove();
  1831. const settingsUI = document.getElementById('settings-container');
  1832. if (settingsUI) settingsUI.remove();
  1833. const statusIndicator = document.getElementById('status-indicator');
  1834. if (statusIndicator) statusIndicator.remove();
  1835. // Clean up all description elements
  1836. cleanupDescriptionElements();
  1837. console.log("X/Twitter Tweet De-Sloppification Deactivated.");
  1838. });
  1839. } else {
  1840. setTimeout(initializeObserver, 1000);
  1841. }
  1842. }
  1843. // Start observing tweets and initializing the UI
  1844. initializeObserver();
  1845. })();
  1846.  
  1847. // ----- config.js -----
  1848. const processedTweets = new Set(); // Set of tweet IDs already processed in this session
  1849. const tweetIDRatingCache = {}; // ID-based cache for persistent storage
  1850. const PROCESSING_DELAY_MS = 500; // Delay before processing a tweet (ms)
  1851. const API_CALL_DELAY_MS = 250; // Minimum delay between API calls (ms)
  1852. let USER_DEFINED_INSTRUCTIONS = GM_getValue('userDefinedInstructions', `- Give high scores to insightful and impactful tweets
  1853. - Give low scores to clickbait, fearmongering, and ragebait
  1854. - Give high scores to high-effort content and artistic content`);
  1855. let currentFilterThreshold = GM_getValue('filterThreshold', 1); // Filter threshold for tweet visibility
  1856. let observedTargetNode = null;
  1857. let lastAPICallTime = 0;
  1858. let pendingRequests = 0;
  1859. const MAX_RETRIES = 3;
  1860. let availableModels = []; // List of models fetched from API
  1861. let selectedModel = GM_getValue('selectedModel', 'google/gemini-flash-1.5-8b');
  1862. let selectedImageModel = GM_getValue('selectedImageModel', 'google/gemini-flash-1.5-8b');
  1863. let blacklistedHandles = GM_getValue('blacklistedHandles', '').split('\n').filter(h => h.trim() !== '');
  1864.  
  1865. let storedRatings = GM_getValue('tweetRatings', '{}');
  1866. // Settings variables
  1867. let enableImageDescriptions = GM_getValue('enableImageDescriptions', false);
  1868.  
  1869.  
  1870. // Model parameters
  1871. let modelTemperature = GM_getValue('modelTemperature', 0.5);
  1872. let modelTopP = GM_getValue('modelTopP', 0.9);
  1873. let imageModelTemperature = GM_getValue('imageModelTemperature', 0.5);
  1874. let imageModelTopP = GM_getValue('imageModelTopP', 0.9);
  1875. let maxTokens = GM_getValue('maxTokens', 0); // Maximum number of tokens for API requests, 0 means no limit
  1876. let imageModelMaxTokens = GM_getValue('imageModelMaxTokens', 0); // Maximum number of tokens for image model API requests, 0 means no limit
  1877. //let menuHTML= "";
  1878.  
  1879. // ----- DOM Selectors (for tweet elements) -----
  1880. const TWEET_ARTICLE_SELECTOR = 'article[data-testid="tweet"]';
  1881. const QUOTE_CONTAINER_SELECTOR = 'div[role="link"][tabindex="0"]';
  1882. const USER_NAME_SELECTOR = 'div[data-testid="User-Name"] span > span';
  1883. const USER_HANDLE_SELECTOR = 'div[data-testid="User-Name"] a[role="link"]';
  1884. const TWEET_TEXT_SELECTOR = 'div[data-testid="tweetText"]';
  1885. const MEDIA_IMG_SELECTOR = 'div[data-testid="tweetPhoto"] img, img[src*="pbs.twimg.com/media"]';
  1886. const MEDIA_VIDEO_SELECTOR = 'video[poster*="pbs.twimg.com"], video';
  1887. const PERMALINK_SELECTOR = 'a[href*="/status/"] time';
  1888. // ----- Dom Elements -----
  1889. /**
  1890. * Helper function to check if a model supports images based on its architecture
  1891. * @param {string} modelId - The model ID to check
  1892. * @returns {boolean} - Whether the model supports image input
  1893. */
  1894. function modelSupportsImages(modelId) {
  1895. if (!availableModels || availableModels.length === 0) {
  1896. return false; // If we don't have model info, assume it doesn't support images
  1897. }
  1898.  
  1899. const model = availableModels.find(m => m.slug === modelId);
  1900. if (!model) {
  1901. return false; // Model not found in available models list
  1902. }
  1903.  
  1904. // Check if model supports images based on its architecture
  1905. return model.input_modalities &&
  1906. model.input_modalities.includes('image');
  1907. }
  1908.  
  1909. try {
  1910. Object.assign(tweetIDRatingCache, JSON.parse(storedRatings));
  1911. console.log(`Loaded ${Object.keys(tweetIDRatingCache).length} cached tweet ratings`);
  1912. } catch (e) {
  1913. console.error('Error loading stored ratings:', e);
  1914. }
  1915.  
  1916.  
  1917. // ----- api.js -----
  1918. /**
  1919. * @typedef {Object} CompletionResponse
  1920. * @property {string} id - Response ID from OpenRouter
  1921. * @property {string} model - Model used for completion
  1922. * @property {Array<{
  1923. * message: {
  1924. * role: string,
  1925. * content: string
  1926. * },
  1927. * finish_reason: string,
  1928. * index: number
  1929. * }>} choices - Array of completion choices
  1930. * @property {Object} usage - Token usage statistics
  1931. * @property {number} usage.prompt_tokens - Number of tokens in prompt
  1932. * @property {number} usage.completion_tokens - Number of tokens in completion
  1933. * @property {number} usage.total_tokens - Total tokens used
  1934. */
  1935.  
  1936. /**
  1937. * @typedef {Object} CompletionRequest
  1938. * @property {string} model - Model ID to use
  1939. * @property {Array<{role: string, content: Array<{type: string, text?: string, image_url?: {url: string}}>}>} messages - Messages for completion
  1940. * @property {number} temperature - Temperature for sampling
  1941. * @property {number} top_p - Top P for sampling
  1942. * @property {number} max_tokens - Maximum tokens to generate
  1943. * @property {Object} provider - Provider settings
  1944. * @property {string} provider.sort - Sort order for models
  1945. * @property {boolean} provider.allow_fallbacks - Whether to allow fallback models
  1946. */
  1947.  
  1948. /**
  1949. * @typedef {Object} CompletionResult
  1950. * @property {boolean} error - Whether an error occurred
  1951. * @property {string} message - Error or success message
  1952. * @property {CompletionResponse|null} data - The completion response data if successful
  1953. */
  1954.  
  1955. /**
  1956. * Gets a completion from OpenRouter API
  1957. *
  1958. * @param {CompletionRequest} request - The completion request
  1959. * @param {string} apiKey - OpenRouter API key
  1960. * @param {number} [timeout=30000] - Request timeout in milliseconds
  1961. * @returns {Promise<CompletionResult>} The completion result
  1962. */
  1963. async function getCompletion(request, apiKey, timeout = 30000) {
  1964. return new Promise((resolve) => {
  1965. GM_xmlhttpRequest({
  1966. method: "POST",
  1967. url: "https://openrouter.ai/api/v1/chat/completions",
  1968. headers: {
  1969. "Content-Type": "application/json",
  1970. "Authorization": `Bearer ${apiKey}`,
  1971. "HTTP-Referer": "https://greasyfork.org/en/scripts/532182-twitter-x-ai-tweet-filter",
  1972. "X-Title": "Tweet Rating Tool"
  1973. },
  1974. data: JSON.stringify(request),
  1975. timeout: timeout,
  1976. onload: function (response) {
  1977. if (response.status >= 200 && response.status < 300) {
  1978. try {
  1979. const data = JSON.parse(response.responseText);
  1980. resolve({
  1981. error: false,
  1982. message: "Request successful",
  1983. data: data
  1984. });
  1985. } catch (error) {
  1986. resolve({
  1987. error: true,
  1988. message: `Failed to parse response: ${error.message}`,
  1989. data: null
  1990. });
  1991. }
  1992. } else {
  1993. resolve({
  1994. error: true,
  1995. message: `Request failed with status ${response.status}: ${response.responseText}`,
  1996. data: null
  1997. });
  1998. }
  1999. },
  2000. onerror: function (error) {
  2001. resolve({
  2002. error: true,
  2003. message: `Request error: ${error.toString()}`,
  2004. data: null
  2005. });
  2006. },
  2007. ontimeout: function () {
  2008. resolve({
  2009. error: true,
  2010. message: `Request timed out after ${timeout}ms`,
  2011. data: null
  2012. });
  2013. }
  2014. });
  2015. });
  2016. }
  2017. const safetySettings = [
  2018. {
  2019. category: "HARM_CATEGORY_HARASSMENT",
  2020. threshold: "BLOCK_NONE",
  2021. },
  2022. {
  2023. category: "HARM_CATEGORY_HATE_SPEECH",
  2024. threshold: "BLOCK_NONE",
  2025. },
  2026. {
  2027. category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
  2028. threshold: "BLOCK_NONE",
  2029. },
  2030. {
  2031. category: "HARM_CATEGORY_DANGEROUS_CONTENT",
  2032. threshold: "BLOCK_NONE",
  2033. },
  2034. {
  2035. category: "HARM_CATEGORY_CIVIC_INTEGRITY",
  2036. threshold: "BLOCK_NONE",
  2037. },
  2038. ];
  2039. /**
  2040. * Rates a tweet using the OpenRouter API with automatic retry functionality.
  2041. *
  2042. * @param {string} tweetText - The text content of the tweet
  2043. * @param {string} tweetId - The unique tweet ID
  2044. * @param {string} apiKey - The API key for authentication
  2045. * @param {string[]} mediaUrls - Array of media URLs associated with the tweet
  2046. * @param {number} [maxRetries=3] - Maximum number of retry attempts
  2047. * @returns {Promise<{score: number, content: string, error: boolean}>} The rating result
  2048. */
  2049. async function rateTweetWithOpenRouter(tweetText, tweetId, apiKey, mediaUrls, maxRetries = 3) {
  2050. const request = {
  2051. model: selectedModel,
  2052. messages: [{
  2053. role: "user",
  2054. content: [{
  2055. type: "text",
  2056. text: `
  2057. You will be given a Tweet, structured like this:
  2058. _______TWEET SCHEMA_______
  2059. _______BEGIN TWEET_______
  2060. [TWEET TweetID]
  2061. [the text of the tweet being replied to]
  2062. [MEDIA_DESCRIPTION]:
  2063. [IMAGE 1]: [description], [IMAGE 2]: [description], etc.
  2064. [REPLY] (if the author is replying to another tweet)
  2065. [TWEET TweetID]: (the tweet which you are to review)
  2066. @[the author of the tweet]
  2067. [the text of the tweet]
  2068. [MEDIA_DESCRIPTION]:
  2069. [IMAGE 1]: [description], [IMAGE 2]: [description], etc.
  2070. [QUOTED_TWEET]: (if the author is quoting another tweet)
  2071. [the text of the quoted tweet]
  2072. [QUOTED_TWEET_MEDIA_DESCRIPTION]:
  2073. [IMAGE 1]: [description], [IMAGE 2]: [description], etc.
  2074. _______END TWEET_______
  2075. _______END TWEET SCHEMA_______
  2076.  
  2077. You are an expert critic of tweets. You are to review and provide a rating for the tweet with tweet ID ${tweetId}.
  2078. Ensure that you consider these user-defined instructions in your analysis and scoring:
  2079. [USER-DEFINED INSTRUCTIONS]:
  2080. ${USER_DEFINED_INSTRUCTIONS}
  2081. Provide a concise explanation of your reasoning and then, on a new line, output your final rating in the exact format:
  2082. SCORE_X where X is a number from 1 (lowest quality) to 10 (highest quality).
  2083. for example: SCORE_1, SCORE_2, SCORE_3, etc.
  2084. If one of the above is not present, the program will not be able to parse the response and will return an error.
  2085. _______BEGIN TWEET_______
  2086. ${tweetText}
  2087. _______END TWEET_______`
  2088. }]
  2089. }],
  2090. temperature: modelTemperature,
  2091. top_p: modelTopP,
  2092. max_tokens: maxTokens,
  2093. provider: {
  2094. sort: GM_getValue('modelSortOrder', 'throughput-high-to-low').split('-')[0],
  2095. allow_fallbacks: true
  2096. }
  2097. };
  2098. if (selectedModel.includes('gemini')){
  2099. request.config = {
  2100. safetySettings: safetySettings,
  2101. }
  2102. }
  2103.  
  2104. // Add image URLs if present and supported
  2105. if (mediaUrls?.length > 0 && modelSupportsImages(selectedModel)) {
  2106. for (const url of mediaUrls) {
  2107. request.messages[0].content.push({
  2108. type: "image_url",
  2109. image_url: { url }
  2110. });
  2111. }
  2112. }
  2113.  
  2114. // Add model parameters
  2115. request.temperature = modelTemperature;
  2116. request.top_p = modelTopP;
  2117. request.max_tokens = maxTokens;
  2118.  
  2119. // Add provider settings
  2120. const sortOrder = GM_getValue('modelSortOrder', 'throughput-high-to-low');
  2121. request.provider = {
  2122. sort: sortOrder.split('-')[0],
  2123. allow_fallbacks: true
  2124. };
  2125.  
  2126. // Implement retry logic with exponential backoff
  2127. let attempt = 0;
  2128. while (attempt < maxRetries) {
  2129. attempt++;
  2130.  
  2131. // Rate limiting
  2132. const now = Date.now();
  2133. const timeElapsed = now - lastAPICallTime;
  2134. if (timeElapsed < API_CALL_DELAY_MS) {
  2135. await new Promise(resolve => setTimeout(resolve, API_CALL_DELAY_MS - timeElapsed));
  2136. }
  2137. lastAPICallTime = now;
  2138.  
  2139. // Update status
  2140. pendingRequests++;
  2141. showStatus(`Rating tweet... (${pendingRequests} pending)`);
  2142.  
  2143. // Make API request
  2144. const result = await getCompletion(request, apiKey);
  2145. pendingRequests--;
  2146. showStatus(`Rating tweet... (${pendingRequests} pending)`);
  2147.  
  2148. if (!result.error && result.data?.choices?.[0]?.message?.content) {
  2149. const content = result.data.choices[0].message.content;
  2150. const scoreMatch = content.match(/\SCORE_(\d+)/);
  2151.  
  2152. if (scoreMatch) {
  2153. const score = parseInt(scoreMatch[1], 10);
  2154. tweetIDRatingCache[tweetId] = {
  2155. tweetContent: tweetText,
  2156. score: score,
  2157. description: content
  2158. };
  2159. saveTweetRatings();
  2160. return { score, content, error: false };
  2161. }
  2162. }
  2163.  
  2164. // Handle retries
  2165. if (attempt < maxRetries) {
  2166. const backoffDelay = Math.pow(attempt, 2) * 1000;
  2167. console.log(`Attempt ${attempt}/${maxRetries} failed. Retrying in ${backoffDelay}ms...`);
  2168. console.log('Response:', {
  2169. error: result.error,
  2170. message: result.message,
  2171. data: result.data,
  2172. content: result.data?.choices?.[0]?.message?.content
  2173. });
  2174. await new Promise(resolve => setTimeout(resolve, backoffDelay));
  2175. }
  2176. }
  2177.  
  2178. return {
  2179. score: 5,
  2180. content: "Failed to get valid rating after multiple attempts",
  2181. error: true
  2182. };
  2183. }
  2184.  
  2185. /**
  2186. * Gets descriptions for images using the OpenRouter API
  2187. *
  2188. * @param {string[]} urls - Array of image URLs to get descriptions for
  2189. * @param {string} apiKey - The API key for authentication
  2190. * @param {string} tweetId - The unique tweet ID
  2191. * @param {string} userHandle - The Twitter user handle
  2192. * @returns {Promise<string>} Combined image descriptions
  2193. */
  2194. async function getImageDescription(urls, apiKey, tweetId, userHandle) {
  2195. if (!urls?.length || !enableImageDescriptions) {
  2196. return !enableImageDescriptions ? '[Image descriptions disabled]' : '';
  2197. }
  2198.  
  2199. let descriptions = [];
  2200. for (const url of urls) {
  2201. const request = {
  2202. model: selectedImageModel,
  2203. messages: [{
  2204. role: "user",
  2205. content: [
  2206. {
  2207. type: "text",
  2208. 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."
  2209. },
  2210. {
  2211. type: "image_url",
  2212. image_url: { url }
  2213. }
  2214. ]
  2215. }],
  2216. temperature: imageModelTemperature,
  2217. top_p: imageModelTopP,
  2218. max_tokens: maxTokens,
  2219. provider: {
  2220. sort: GM_getValue('modelSortOrder', 'throughput-high-to-low').split('-')[0],
  2221. allow_fallbacks: true
  2222. }
  2223. };
  2224. if (selectedImageModel.includes('gemini')){
  2225. request.config = {
  2226. safetySettings: safetySettings,
  2227. }
  2228. }
  2229. const result = await getCompletion(request, apiKey);
  2230. if (!result.error && result.data?.choices?.[0]?.message?.content) {
  2231. descriptions.push(result.data.choices[0].message.content);
  2232. } else {
  2233. descriptions.push('[Error getting image description]');
  2234. }
  2235. }
  2236.  
  2237. return descriptions.map((desc, i) => `[IMAGE ${i + 1}]: ${desc}`).join('\n');
  2238. }
  2239.  
  2240. /**
  2241. * Fetches the list of available models from the OpenRouter API.
  2242. * Uses the stored API key, and updates the model selector upon success.
  2243. */
  2244. function fetchAvailableModels() {
  2245. const apiKey = GM_getValue('openrouter-api-key', '');
  2246. if (!apiKey) {
  2247. console.log('No API key available, skipping model fetch');
  2248. showStatus('Please enter your OpenRouter API key');
  2249. return;
  2250. }
  2251. showStatus('Fetching available models...');
  2252. const sortOrder = GM_getValue('modelSortOrder', 'throughput-high-to-low');
  2253. GM_xmlhttpRequest({
  2254. method: "GET",
  2255. url: `https://openrouter.ai/api/frontend/models/find?order=${sortOrder}`,
  2256. headers: {
  2257. "Authorization": `Bearer ${apiKey}`,
  2258. "HTTP-Referer": "https://greasyfork.org/en/scripts/532182-twitter-x-ai-tweet-filter", // Use a more generic referer if preferred
  2259. "X-Title": "Tweet Rating Tool"
  2260. },
  2261. onload: function (response) {
  2262. try {
  2263. const data = JSON.parse(response.responseText);
  2264. if (data.data && data.data.models) {
  2265. availableModels = data.data.models || [];
  2266. refreshModelsUI();
  2267. showStatus('Models updated!');
  2268. }
  2269. } catch (error) {
  2270. console.error('Error parsing model list:', error);
  2271. showStatus('Error parsing models list');
  2272. }
  2273. },
  2274. onerror: function (error) {
  2275. console.error('Error fetching models:', error);
  2276. showStatus('Error fetching models!');
  2277. }
  2278. });
  2279. }
  2280.  
  2281.  
  2282. // ----- domScraper.js -----
  2283. /**
  2284. * Extracts and returns trimmed text content from the given element(s).
  2285. * @param {Node|NodeList} elements - A DOM element or a NodeList.
  2286. * @returns {string} The trimmed text content.
  2287. */
  2288. function getElementText(elements) {
  2289. if (!elements) return '';
  2290. const elementList = elements instanceof NodeList ? Array.from(elements) : [elements];
  2291. for (const element of elementList) {
  2292. const text = element?.textContent?.trim();
  2293. if (text) return text;
  2294. }
  2295. return '';
  2296. }
  2297. /**
  2298. * Extracts the tweet ID from a tweet article element.
  2299. * @param {Element} tweetArticle - The tweet article element.
  2300. * @returns {string} The tweet ID.
  2301. */
  2302. function getTweetID(tweetArticle) {
  2303. const timeEl = tweetArticle.querySelector(PERMALINK_SELECTOR);
  2304. let tweetId = timeEl?.parentElement?.href;
  2305. if (tweetId && tweetId.includes('/status/')) {
  2306. const match = tweetId.match(/\/status\/(\d+)/);
  2307. if (match && match[1]) {
  2308. return match[1];
  2309. }
  2310. return tweetId.substring(tweetId.indexOf('/status/') + 1);
  2311. }
  2312. return `tweet-${Math.random().toString(36).substring(2, 15)}-${Date.now()}`;
  2313. }
  2314.  
  2315. /**
  2316. * Extracts the Twitter handle from a tweet article element.
  2317. * @param {Element} tweetArticle - The tweet article element.
  2318. * @returns {array} The user and quoted user handles.
  2319. */
  2320. function getUserHandles(tweetArticle) {
  2321. const handleElement = tweetArticle.querySelectorAll(USER_HANDLE_SELECTOR);
  2322. let handles=[];
  2323. if (handleElement) {
  2324. /*
  2325. const href = handleElement.getAttribute('href');
  2326. if (href && href.startsWith('/')) {
  2327. return href.slice(1);
  2328. }
  2329. */
  2330. handleElement.forEach(element => {
  2331. const href = element.getAttribute('href');
  2332. if (href && href.startsWith('/')) {
  2333. handles.push(href.slice(1));
  2334. }
  2335. });
  2336. }
  2337. return handles.length>0?handles:[''];
  2338. }
  2339.  
  2340.  
  2341. /**
  2342. * Extracts and returns an array of media URLs from the tweet element.
  2343. * @param {Element} scopeElement - The tweet element.
  2344. * @param {string} tweetIdForDebug - The tweet ID (for logging).
  2345. * @returns {string[]} An array of media URLs.
  2346. */
  2347. function extractMediaLinks(scopeElement) {
  2348. if (!scopeElement) return [];
  2349. const mediaLinks = new Set();
  2350. // Find all images and videos in the tweet
  2351. scopeElement.querySelectorAll(`${MEDIA_IMG_SELECTOR}, ${MEDIA_VIDEO_SELECTOR}`).forEach(mediaEl => {
  2352. // Get the source URL (src for images, poster for videos)
  2353. const sourceUrl = mediaEl.tagName === 'IMG' ? mediaEl.src : mediaEl.poster;
  2354. // Skip if not a Twitter media URL
  2355. /*
  2356. if (!sourceUrl ||
  2357. !(sourceUrl.includes('pbs.twimg.com/') ||
  2358. sourceUrl.includes('pbs.twimg.com/amplify_video_thumb'))) {
  2359. return;
  2360. }*/
  2361. try {
  2362. // Parse the URL to handle format parameters
  2363. const url = new URL(sourceUrl);
  2364. const format = url.searchParams.get('format');
  2365. const name = url.searchParams.get('name'); // 'small', 'medium', 'large', etc.
  2366. // Create the final URL with the right format and size
  2367. let finalUrl = sourceUrl;
  2368. // Try to get the original size by removing size indicator
  2369. if (name && name !== 'orig') {
  2370. // Replace format=jpg&name=small with format=jpg&name=orig
  2371. finalUrl = sourceUrl.replace(`name=${name}`, 'name=orig');
  2372. }
  2373. // Log both the original and final URLs for debugging
  2374. //console.log(`[Tweet ${tweetIdForDebug}] Processing media: ${sourceUrl} → ${finalUrl}`);
  2375. mediaLinks.add(finalUrl);
  2376. } catch (error) {
  2377. //console.error(`[Tweet ${tweetIdForDebug}] Error processing media URL: ${sourceUrl}`, error);
  2378. // Fallback: just add the raw URL as is
  2379. mediaLinks.add(sourceUrl);
  2380. }
  2381. });
  2382. return Array.from(mediaLinks);
  2383. }
  2384.  
  2385. // ----- Rating Indicator Functions -----
  2386.  
  2387. /**
  2388. * Processes a single tweet after a delay.
  2389. * It first sets a pending indicator, then either applies a cached rating,
  2390. * or calls the API to rate the tweet (with retry logic).
  2391. * Finally, it applies the filtering logic.
  2392. * @param {Element} tweetArticle - The tweet element.
  2393. * @param {string} tweetId - The tweet ID.
  2394. */
  2395. // Helper function to determine if a tweet is the original tweet in a conversation.
  2396. // We check if the tweet article has a following sibling with data-testid="inline_reply_offscreen".
  2397. function isOriginalTweet(tweetArticle) {
  2398. let sibling = tweetArticle.nextElementSibling;
  2399. while (sibling) {
  2400. if (sibling.matches && sibling.matches('div[data-testid="inline_reply_offscreen"]')) {
  2401. return true;
  2402. }
  2403. sibling = sibling.nextElementSibling;
  2404. }
  2405. return false;
  2406. }
  2407.  
  2408.  
  2409. // ----- MutationObserver Setup -----
  2410. /**
  2411. * Handles DOM mutations to detect new tweets added to the timeline.
  2412. * @param {MutationRecord[]} mutationsList - List of observed mutations.
  2413. */
  2414. function handleMutations(mutationsList) {
  2415.  
  2416. for (const mutation of mutationsList) {
  2417. handleThreads();
  2418. if (mutation.type === 'childList') {
  2419. // Process added nodes
  2420. if (mutation.addedNodes.length > 0) {
  2421. mutation.addedNodes.forEach(node => {
  2422. if (node.nodeType === Node.ELEMENT_NODE) {
  2423. if (node.matches && node.matches(TWEET_ARTICLE_SELECTOR)) {
  2424. scheduleTweetProcessing(node);
  2425. }
  2426. else if (node.querySelectorAll) {
  2427. const tweetsInside = node.querySelectorAll(TWEET_ARTICLE_SELECTOR);
  2428. tweetsInside.forEach(scheduleTweetProcessing);
  2429. }
  2430. }
  2431. });
  2432. }
  2433.  
  2434. // Process removed nodes to clean up description elements
  2435. if (mutation.removedNodes.length > 0) {
  2436. mutation.removedNodes.forEach(node => {
  2437. if (node.nodeType === Node.ELEMENT_NODE) {
  2438. // Check if the removed node is a tweet article or contains tweet articles
  2439. const isTweet = node.matches && node.matches(TWEET_ARTICLE_SELECTOR);
  2440. const removedTweets = isTweet ? [node] :
  2441. (node.querySelectorAll ? Array.from(node.querySelectorAll(TWEET_ARTICLE_SELECTOR)) : []);
  2442.  
  2443. // For each removed tweet, find and remove its description element
  2444. removedTweets.forEach(tweet => {
  2445. const indicator = tweet.querySelector('.score-indicator');
  2446. if (indicator && indicator.dataset.id) {
  2447. const descId = 'desc-' + indicator.dataset.id;
  2448. const descBox = document.getElementById(descId);
  2449. if (descBox) {
  2450. descBox.remove();
  2451. //console.debug(`Removed description box ${descId} for tweet that was removed from the DOM`);
  2452. }
  2453. }
  2454. });
  2455. }
  2456. });
  2457. }
  2458. }
  2459. }
  2460. }
  2461. // ----- ratingEngine.js -----
  2462. /**
  2463. * Applies filtering to a single tweet by hiding it if its score is below the threshold.
  2464. * Also updates the rating indicator.
  2465. * @param {Element} tweetArticle - The tweet element.
  2466. */
  2467. function filterSingleTweet(tweetArticle) {
  2468. const score = parseInt(tweetArticle.dataset.sloppinessScore || '0', 10);
  2469. // Update the indicator based on the tweet's rating status
  2470. setScoreIndicator(tweetArticle, score, tweetArticle.dataset.ratingStatus || 'rated', tweetArticle.dataset.ratingDescription);
  2471. // If the tweet is still pending a rating, keep it visible
  2472. if (tweetArticle.dataset.ratingStatus === 'pending') {
  2473. tweetArticle.style.display = '';
  2474. } else if (isNaN(score) || score < currentFilterThreshold) {
  2475. tweetArticle.style.display = 'none';
  2476. } else {
  2477. tweetArticle.style.display = '';
  2478. }
  2479. }
  2480.  
  2481. /**
  2482. * Applies a cached rating (if available) to a tweet article.
  2483. * Also sets the rating status to 'rated' and updates the indicator.
  2484. * @param {Element} tweetArticle - The tweet element.
  2485. * @returns {boolean} True if a cached rating was applied.
  2486. */
  2487. function applyTweetCachedRating(tweetArticle) {
  2488. const tweetId = getTweetID(tweetArticle);
  2489. const handles = getUserHandles(tweetArticle);
  2490. const userHandle = handles.length > 0 ? handles[0] : '';
  2491. // Blacklisted users are automatically given a score of 10
  2492. if (userHandle && isUserBlacklisted(userHandle)) {
  2493. //console.debug(`Blacklisted user detected: ${userHandle}, assigning score 10`);
  2494. tweetArticle.dataset.sloppinessScore = '10';
  2495. tweetArticle.dataset.blacklisted = 'true';
  2496. tweetArticle.dataset.ratingStatus = 'blacklisted';
  2497. tweetArticle.dataset.ratingDescription = 'Whtielisted user';
  2498. setScoreIndicator(tweetArticle, 10, 'blacklisted', "User is whitelisted");
  2499. filterSingleTweet(tweetArticle);
  2500. return true;
  2501. }
  2502. // Check ID-based cache
  2503. if (tweetIDRatingCache[tweetId]) {
  2504. const score = tweetIDRatingCache[tweetId].score;
  2505. const desc = tweetIDRatingCache[tweetId].description;
  2506. //console.debug(`Applied cached rating for tweet ${tweetId}: ${score}`);
  2507. tweetArticle.dataset.sloppinessScore = score.toString();
  2508. tweetArticle.dataset.cachedRating = 'true';
  2509. tweetArticle.dataset.ratingStatus = 'cached';
  2510. tweetArticle.dataset.ratingDescription = desc;
  2511. setScoreIndicator(tweetArticle, score, 'cached', desc);
  2512. filterSingleTweet(tweetArticle);
  2513. return true;
  2514. }
  2515.  
  2516. return false;
  2517. }
  2518. // ----- UI Helper Functions -----
  2519.  
  2520. /**
  2521. * Saves the tweet ratings (by tweet ID) to persistent storage.
  2522. */
  2523. function saveTweetRatings() {
  2524. GM_setValue('tweetRatings', JSON.stringify(tweetIDRatingCache));
  2525. //console.log(`Saved ${Object.keys(tweetIDRatingCache).length} tweet ratings to storage`);
  2526. }
  2527. /**
  2528. * Checks if a given user handle is in the blacklist.
  2529. * @param {string} handle - The Twitter handle.
  2530. * @returns {boolean} True if blacklisted, false otherwise.
  2531. */
  2532. function isUserBlacklisted(handle) {
  2533. if (!handle) return false;
  2534. handle = handle.toLowerCase().trim();
  2535. return blacklistedHandles.some(h => h.toLowerCase().trim() === handle);
  2536. }
  2537.  
  2538. async function delayedProcessTweet(tweetArticle, tweetId) {
  2539. const apiKey = GM_getValue('openrouter-api-key', '');
  2540. if (!apiKey) {
  2541. tweetArticle.dataset.ratingStatus = 'error';
  2542. tweetArticle.dataset.ratingDescription = "No API key";
  2543. setScoreIndicator(tweetArticle, 5, 'error', "No API key");
  2544. filterSingleTweet(tweetArticle);
  2545. return;
  2546. }
  2547. let score = 5; // Default score if rating fails
  2548. let description = "";
  2549.  
  2550. try {
  2551. // Get user handle
  2552. const handles = getUserHandles(tweetArticle);
  2553. const userHandle = handles.length > 0 ? handles[0] : '';
  2554. const quotedHandle = handles.length > 1 ? handles[1] : '';
  2555. const allMediaLinks = extractMediaLinks(tweetArticle);
  2556. // Check if tweet's author is blacklisted (fast path)
  2557. if (userHandle && isUserBlacklisted(userHandle)) {
  2558. tweetArticle.dataset.sloppinessScore = '10';
  2559. tweetArticle.dataset.blacklisted = 'true';
  2560. tweetArticle.dataset.ratingStatus = 'blacklisted';
  2561. tweetArticle.dataset.ratingDescription = "Blacklisted user";
  2562. setScoreIndicator(tweetArticle, 10, 'blacklisted', "User is blacklisted");
  2563. filterSingleTweet(tweetArticle);
  2564. return;
  2565. }
  2566. // Check if a cached rating exists
  2567. if (applyTweetCachedRating(tweetArticle)) {
  2568. return;
  2569. }
  2570. const fullContextWithImageDescription = await getFullContext(tweetArticle, tweetId, apiKey);
  2571. // --- API Call or Fallback ---
  2572. if (apiKey && fullContextWithImageDescription) {
  2573. try {
  2574. const rating = await rateTweetWithOpenRouter(fullContextWithImageDescription, tweetId, apiKey, allMediaLinks);
  2575. score = rating.score;
  2576. description = rating.content;
  2577. tweetArticle.dataset.ratingStatus = rating.error ? 'error' : 'rated';
  2578. tweetArticle.dataset.ratingDescription = description || "not available";
  2579. tweetArticle.dataset.sloppinessScore = score.toString();
  2580. setScoreIndicator(tweetArticle, score, tweetArticle.dataset.ratingStatus, tweetArticle.dataset.ratingDescription);
  2581. filterSingleTweet(tweetArticle);
  2582.  
  2583. } catch (apiError) {
  2584. score = Math.floor(Math.random() * 10) + 1; // Fallback to a random score
  2585. tweetArticle.dataset.ratingStatus = 'error';
  2586. }
  2587. } else {
  2588. // If there's no API key or textual content (e.g., only media), use a fallback random score.
  2589. score = Math.floor(Math.random() * 10) + 1;
  2590. tweetArticle.dataset.ratingStatus = 'rated';
  2591. }
  2592.  
  2593. tweetArticle.dataset.sloppinessScore = score.toString();
  2594. filterSingleTweet(tweetArticle);
  2595. // Log all collected information at once
  2596. console.log(`Tweet ${tweetId}:
  2597. ${fullContextWithImageDescription} - ${score} Model response: - ${description}`);
  2598.  
  2599. } catch (error) {
  2600. if (!tweetArticle.dataset.sloppinessScore) {
  2601. tweetArticle.dataset.sloppinessScore = '5';
  2602. tweetArticle.dataset.ratingStatus = 'error';
  2603. tweetArticle.dataset.ratingDescription = "error processing tweet";
  2604. console.error(`Error processing tweet ${tweetId}: ${error}`);
  2605. setScoreIndicator(tweetArticle, 5, 'error', 'Error processing tweet');
  2606. filterSingleTweet(tweetArticle);
  2607. }
  2608. }
  2609. }
  2610.  
  2611. /**
  2612. * Schedules processing of a tweet if it hasn't been processed yet.
  2613. * @param {Element} tweetArticle - The tweet element.
  2614. */
  2615. function scheduleTweetProcessing(tweetArticle) {
  2616. const tweetId = getTweetID(tweetArticle);
  2617. // Fast-path: if author is blacklisted, assign score immediately
  2618. const handles = getUserHandles(tweetArticle);
  2619. const userHandle = handles.length > 0 ? handles[0] : '';
  2620. if (userHandle && isUserBlacklisted(userHandle)) {
  2621. tweetArticle.dataset.sloppinessScore = '10';
  2622. tweetArticle.dataset.blacklisted = 'true';
  2623. tweetArticle.dataset.ratingStatus = 'rated';
  2624.  
  2625. tweetArticle.dataset.ratingDescription = "Whitelisted user";
  2626. setScoreIndicator(tweetArticle, 10, 'blacklisted', "User is whitelisted");
  2627. filterSingleTweet(tweetArticle);
  2628. return;
  2629. }
  2630. // If a cached rating is available, use it immediately
  2631. if (tweetIDRatingCache[tweetId]) {
  2632. applyTweetCachedRating(tweetArticle);
  2633. return;
  2634. }
  2635. // Skip if already processed in this session
  2636. if (processedTweets.has(tweetId)) {
  2637. return;
  2638. }
  2639.  
  2640. // Immediately mark as pending before scheduling actual processing
  2641. processedTweets.add(tweetId);
  2642. tweetArticle.dataset.ratingStatus = 'pending';
  2643. setScoreIndicator(tweetArticle, null, 'pending');
  2644.  
  2645. // Now schedule the actual rating processing
  2646. setTimeout(() => { delayedProcessTweet(tweetArticle, tweetId); }, PROCESSING_DELAY_MS);
  2647. }
  2648.  
  2649.  
  2650. /**
  2651. * Extracts the full context of a tweet article and returns a formatted string.
  2652. *
  2653. * Schema:
  2654. * [TWEET]:
  2655. * @[the author of the tweet]
  2656. * [the text of the tweet]
  2657. * [MEDIA_DESCRIPTION]:
  2658. * [IMAGE 1]: [description], [IMAGE 2]: [description], etc.
  2659. * [QUOTED_TWEET]:
  2660. * [the text of the quoted tweet]
  2661. * [QUOTED_TWEET_MEDIA_DESCRIPTION]:
  2662. * [IMAGE 1]: [description], [IMAGE 2]: [description], etc.
  2663. *
  2664. * @param {Element} tweetArticle - The tweet article element.
  2665. * @param {string} tweetId - The tweet's ID.
  2666. * @param {string} apiKey - API key used for getting image descriptions.
  2667. * @returns {Promise<string>} - The full context string.
  2668. */
  2669. async function getFullContext(tweetArticle, tweetId, apiKey) {
  2670. const handles = getUserHandles(tweetArticle);
  2671. const userHandle = handles.length > 0 ? handles[0] : '';
  2672. const quotedHandle = handles.length > 1 ? handles[1] : '';
  2673. // --- Extract Main Tweet Content ---
  2674. const mainText = getElementText(tweetArticle.querySelector(TWEET_TEXT_SELECTOR));
  2675. let allMediaLinks = extractMediaLinks(tweetArticle);
  2676.  
  2677. // --- Extract Quoted Tweet Content (if any) ---
  2678. let quotedText = "";
  2679. let quotedMediaLinks = [];
  2680. const quoteContainer = tweetArticle.querySelector(QUOTE_CONTAINER_SELECTOR);
  2681. if (quoteContainer) {
  2682. quotedText = getElementText(quoteContainer.querySelector(TWEET_TEXT_SELECTOR));
  2683. quotedMediaLinks = extractMediaLinks(quoteContainer);
  2684. }
  2685. // Remove any media links from the main tweet that also appear in the quoted tweet
  2686. let mainMediaLinks = allMediaLinks.filter(link => !quotedMediaLinks.includes(link));
  2687. let fullContextWithImageDescription = `[TWEET ${tweetId}]
  2688. Author:@${userHandle}:
  2689. ` + mainText;
  2690.  
  2691. if (mainMediaLinks.length > 0) {
  2692. // Process main tweet images only if image descriptions are enabled
  2693. if (enableImageDescriptions) {
  2694. let mainMediaLinksDescription = await getImageDescription(mainMediaLinks, apiKey, tweetId, userHandle);
  2695. fullContextWithImageDescription += `
  2696. [MEDIA_DESCRIPTION]:
  2697. ${mainMediaLinksDescription}`;
  2698. }
  2699. // Just add the URLs when descriptions are disabled
  2700. fullContextWithImageDescription += `
  2701. [MEDIA_URLS]:
  2702. ${mainMediaLinks.join(", ")}`;
  2703. }
  2704. // --- Quoted Tweet Handling ---
  2705. if (quotedText) {
  2706. fullContextWithImageDescription += `
  2707. [QUOTED_TWEET]:
  2708. Author:@${quotedHandle}:
  2709. ${quotedText}`;
  2710. if (quotedMediaLinks.length > 0) {
  2711. // Process quoted tweet images only if image descriptions are enabled
  2712. if (enableImageDescriptions) {
  2713. let quotedMediaLinksDescription = await getImageDescription(quotedMediaLinks, apiKey, tweetId, userHandle);
  2714. fullContextWithImageDescription += `
  2715. [QUOTED_TWEET_MEDIA_DESCRIPTION]:
  2716. ${quotedMediaLinksDescription}`;
  2717. } else {
  2718. // Just add the URLs when descriptions are disabled
  2719. fullContextWithImageDescription += `
  2720. [QUOTED_TWEET_MEDIA_URLS]:
  2721. ${quotedMediaLinks.join(", ")}`;
  2722. }
  2723. }
  2724. }
  2725.  
  2726. // --- Conversation Thread Handling ---
  2727. const conversation = document.querySelector('div[aria-label="Timeline: Conversation"]');
  2728. if (conversation && conversation.dataset.threadHist) {
  2729. // If this tweet is not the original tweet, prepend the thread history.
  2730. if (!isOriginalTweet(tweetArticle)) {
  2731. fullContextWithImageDescription = conversation.dataset.threadHist + `
  2732. [REPLY]
  2733. ` + fullContextWithImageDescription;
  2734. }
  2735. }
  2736. tweetArticle.dataset.fullContext = fullContextWithImageDescription;
  2737. return fullContextWithImageDescription;
  2738. }
  2739.  
  2740.  
  2741. /**
  2742. * Applies filtering to all tweets currently in the observed container.
  2743. */
  2744. function applyFilteringToAll() {
  2745. if (!observedTargetNode) return;
  2746. const tweets = observedTargetNode.querySelectorAll(TWEET_ARTICLE_SELECTOR);
  2747. tweets.forEach(filterSingleTweet);
  2748. }
  2749.  
  2750. /**
  2751. * Periodically checks and processes tweets that might have been added without triggering mutations.
  2752. */
  2753. function ensureAllTweetsRated() {
  2754. if (!observedTargetNode) return;
  2755. const tweets = observedTargetNode.querySelectorAll(TWEET_ARTICLE_SELECTOR);
  2756. tweets.forEach(tweet => {
  2757. if (!tweet.dataset.sloppinessScore) {
  2758. scheduleTweetProcessing(tweet);
  2759. }
  2760. });
  2761. }
  2762.  
  2763. async function handleThreads() {
  2764. let conversation = document.querySelector('div[aria-label="Timeline: Conversation"]');
  2765. if (conversation) {
  2766.  
  2767. if (conversation.dataset.threadHist == undefined) {
  2768.  
  2769. threadHist = "";
  2770. const firstArticle = document.querySelector('article[data-testid="tweet"]');
  2771. if (firstArticle) {
  2772. conversation.dataset.threadHist = 'pending';
  2773. const tweetId = getTweetID(firstArticle);
  2774. if (tweetIDRatingCache[tweetId]) {
  2775. threadHist = tweetIDRatingCache[tweetId].tweetContent;
  2776. } else {
  2777. const apiKey = GM_getValue('openrouter-api-key', '');
  2778. const fullcxt = await getFullContext(firstArticle, tweetId, apiKey);
  2779. threadHist = fullcxt;
  2780. }
  2781. conversation.dataset.threadHist = threadHist;
  2782. //this lets us know if we are still on the main post of the conversation or if we are on a reply to the main post. Will disapear every time we dive deeper
  2783. conversation.firstChild.dataset.canary = "true";
  2784. }
  2785. }
  2786. else if (conversation.dataset.threadHist == "pending") {
  2787. return;
  2788. }
  2789. else if (conversation.dataset.threadHist != "pending" && conversation.firstChild.dataset.canary == undefined) {
  2790. conversation.firstChild.dataset.canary = "pending";
  2791. const nextArticle = document.querySelector('article[data-testid="tweet"]:has(~ div[data-testid="inline_reply_offscreen"])');
  2792. const tweetId = getTweetID(nextArticle);
  2793. if (tweetIDRatingCache[tweetId]) {
  2794. threadHist = threadHist + "\n[REPLY]\n" + tweetIDRatingCache[tweetId].tweetContent;
  2795. } else {
  2796. const apiKey = GM_getValue('openrouter-api-key', '');
  2797. await new Promise(resolve => setTimeout(resolve, 500));
  2798. const newContext = await getFullContext(nextArticle, tweetId, apiKey);
  2799. threadHist = threadHist + "\n[REPLY]\n" + newContext;
  2800. conversation.dataset.threadHist = threadHist;
  2801. }
  2802. }
  2803. }
  2804. }
  2805.  
  2806.  
  2807. // ----- ui.js -----
  2808. // --- Constants ---
  2809. const VERSION = '1.3'; // Update version here
  2810.  
  2811. // --- Utility Functions ---
  2812.  
  2813. /**
  2814. * Displays a temporary status message on the screen.
  2815. * @param {string} message - The message to display.
  2816. */
  2817. function showStatus(message) {
  2818. const indicator = document.getElementById('status-indicator');
  2819. if (!indicator) {
  2820. console.error('#status-indicator element not found.');
  2821. return;
  2822. }
  2823. indicator.textContent = message;
  2824. indicator.classList.add('active');
  2825. setTimeout(() => { indicator.classList.remove('active'); }, 3000);
  2826. }
  2827.  
  2828. /**
  2829. * Toggles the visibility of an element and updates the corresponding toggle button text.
  2830. * @param {HTMLElement} element - The element to toggle.
  2831. * @param {HTMLElement} toggleButton - The button that controls the toggle.
  2832. * @param {string} openText - Text for the button when the element is open.
  2833. * @param {string} closedText - Text for the button when the element is closed.
  2834. */
  2835. function toggleElementVisibility(element, toggleButton, openText, closedText) {
  2836. if (!element || !toggleButton) return;
  2837.  
  2838. const isHidden = element.classList.toggle('hidden');
  2839. toggleButton.innerHTML = isHidden ? closedText : openText;
  2840.  
  2841. // Special case for filter slider button (hide it when panel is shown)
  2842. if (element.id === 'tweet-filter-container') {
  2843. const filterToggle = document.getElementById('filter-toggle');
  2844. if (filterToggle) {
  2845. filterToggle.style.display = isHidden ? 'block' : 'none';
  2846. }
  2847. }
  2848. }
  2849.  
  2850. // --- Core UI Logic ---
  2851.  
  2852. /**
  2853. * Injects the UI elements from the HTML resource into the page.
  2854. */
  2855. function injectUI() {
  2856. //combined userscript has a const named MENU. If it exists, use it.
  2857. let menuHTML;
  2858. if(MENU){
  2859. menuHTML = MENU;
  2860. }else{
  2861. menuHTML = GM_getValue('menuHTML');
  2862. }
  2863. if (!menuHTML) {
  2864. console.error('Failed to load Menu.html resource!');
  2865. showStatus('Error: Could not load UI components.');
  2866. return null;
  2867. }
  2868.  
  2869. // Create a container to inject HTML
  2870. const containerId = 'tweetfilter-root-container'; // Use the ID from the updated HTML
  2871. let uiContainer = document.getElementById(containerId);
  2872. if (uiContainer) {
  2873. console.warn('UI container already exists. Skipping injection.');
  2874. return uiContainer; // Return existing container
  2875. }
  2876.  
  2877. uiContainer = document.createElement('div');
  2878. uiContainer.id = containerId;
  2879. uiContainer.innerHTML = menuHTML;
  2880.  
  2881. // Inject styles
  2882. const stylesheet = uiContainer.querySelector('style');
  2883. if (stylesheet) {
  2884. GM_addStyle(stylesheet.textContent);
  2885. console.log('Injected styles from Menu.html');
  2886. stylesheet.remove(); // Remove style tag after injecting
  2887. } else {
  2888. console.warn('No <style> tag found in Menu.html');
  2889. }
  2890.  
  2891. // Append the rest of the UI elements
  2892. document.body.appendChild(uiContainer);
  2893. console.log('TweetFilter UI Injected from HTML resource.');
  2894.  
  2895. // Set version number
  2896. const versionInfo = uiContainer.querySelector('#version-info');
  2897. if (versionInfo) {
  2898. versionInfo.textContent = `Twitter De-Sloppifier v${VERSION}`;
  2899. }
  2900.  
  2901. return uiContainer; // Return the newly created container
  2902. }
  2903.  
  2904. /**
  2905. * Initializes all UI event listeners using event delegation.
  2906. * @param {HTMLElement} uiContainer - The root container element for the UI.
  2907. */
  2908. function initializeEventListeners(uiContainer) {
  2909. if (!uiContainer) {
  2910. console.error('UI Container not found for event listeners.');
  2911. return;
  2912. }
  2913.  
  2914. console.log('Wiring UI events...');
  2915.  
  2916. const settingsContainer = uiContainer.querySelector('#settings-container');
  2917. const filterContainer = uiContainer.querySelector('#tweet-filter-container');
  2918. const settingsToggleBtn = uiContainer.querySelector('#settings-toggle');
  2919. const filterToggleBtn = uiContainer.querySelector('#filter-toggle');
  2920.  
  2921. // --- Delegated Event Listener for Clicks ---
  2922. uiContainer.addEventListener('click', (event) => {
  2923. const target = event.target;
  2924. const action = target.dataset.action;
  2925. const setting = target.dataset.setting;
  2926. const paramName = target.closest('.parameter-row')?.dataset.paramName;
  2927. const tab = target.dataset.tab;
  2928. const toggleTargetId = target.closest('[data-toggle]')?.dataset.toggle;
  2929.  
  2930. // Button Actions
  2931. if (action) {
  2932. switch (action) {
  2933. case 'close-filter':
  2934. toggleElementVisibility(filterContainer, filterToggleBtn, 'Filter Slider', 'Filter Slider');
  2935. break;
  2936. case 'close-settings':
  2937. toggleElementVisibility(settingsContainer, settingsToggleBtn, '<span style="font-size: 14px;">✕</span> Close', '<span style="font-size: 14px;">⚙️</span> Settings');
  2938. break;
  2939. case 'save-api-key':
  2940. saveApiKey();
  2941. break;
  2942. case 'clear-cache':
  2943. clearTweetRatingsAndRefreshUI();
  2944. break;
  2945. case 'export-settings':
  2946. exportSettings();
  2947. break;
  2948. case 'import-settings':
  2949. importSettings();
  2950. break;
  2951. case 'reset-settings':
  2952. resetSettings();
  2953. break;
  2954. case 'save-instructions':
  2955. saveInstructions();
  2956. break;
  2957. case 'add-handle':
  2958. addHandleFromInput();
  2959. break;
  2960. }
  2961. }
  2962.  
  2963. // Handle List Removal (delegated)
  2964. if (target.classList.contains('remove-handle')) {
  2965. const handleItem = target.closest('.handle-item');
  2966. const handleTextElement = handleItem?.querySelector('.handle-text');
  2967. if (handleTextElement) {
  2968. const handle = handleTextElement.textContent.substring(1); // Remove '@'
  2969. removeHandleFromBlacklist(handle);
  2970. }
  2971. }
  2972.  
  2973. // Tab Switching
  2974. if (tab) {
  2975. switchTab(tab);
  2976. }
  2977.  
  2978. // Advanced Options Toggle
  2979. if (toggleTargetId) {
  2980. toggleAdvancedOptions(toggleTargetId);
  2981. }
  2982. });
  2983.  
  2984. // --- Delegated Event Listener for Input/Change ---
  2985. uiContainer.addEventListener('input', (event) => {
  2986. const target = event.target;
  2987. const setting = target.dataset.setting;
  2988. const paramName = target.closest('.parameter-row')?.dataset.paramName;
  2989.  
  2990. // Settings Inputs / Toggles
  2991. if (setting) {
  2992. handleSettingChange(target, setting);
  2993. }
  2994.  
  2995. // Parameter Controls (Sliders/Number Inputs)
  2996. if (paramName) {
  2997. handleParameterChange(target, paramName);
  2998. }
  2999.  
  3000. // Filter Slider
  3001. if (target.id === 'tweet-filter-slider') {
  3002. handleFilterSliderChange(target);
  3003. }
  3004. });
  3005.  
  3006. uiContainer.addEventListener('change', (event) => {
  3007. const target = event.target;
  3008. const setting = target.dataset.setting;
  3009.  
  3010. // Settings Inputs / Toggles (for selects like sort order)
  3011. if (setting === 'modelSortOrder') {
  3012. handleSettingChange(target, setting);
  3013. fetchAvailableModels(); // Refresh models on sort change
  3014. }
  3015.  
  3016. // Settings Checkbox toggle (need change event for checkboxes)
  3017. if (setting === 'enableImageDescriptions') {
  3018. handleSettingChange(target, setting);
  3019. }
  3020. });
  3021.  
  3022. // --- Direct Event Listeners (Less common cases) ---
  3023.  
  3024. // Settings Toggle Button
  3025. if (settingsToggleBtn) {
  3026. settingsToggleBtn.onclick = () => {
  3027. toggleElementVisibility(settingsContainer, settingsToggleBtn, '<span style="font-size: 14px;">✕</span> Close', '<span style="font-size: 14px;">⚙️</span> Settings');
  3028. };
  3029. }
  3030.  
  3031. // Filter Toggle Button
  3032. if (filterToggleBtn) {
  3033. filterToggleBtn.onclick = () => {
  3034. // Ensure filter container is shown and button is hidden
  3035. if (filterContainer) filterContainer.classList.remove('hidden');
  3036. filterToggleBtn.style.display = 'none';
  3037. };
  3038. }
  3039.  
  3040. // Close custom selects when clicking outside
  3041. document.addEventListener('click', closeAllSelectBoxes);
  3042.  
  3043. console.log('UI events wired.');
  3044. }
  3045.  
  3046. // --- Event Handlers ---
  3047.  
  3048. /** Saves the API key from the input field. */
  3049. function saveApiKey() {
  3050. const apiKeyInput = document.getElementById('openrouter-api-key');
  3051. const apiKey = apiKeyInput.value.trim();
  3052. let previousAPIKey = GM_getValue('openrouter-api-key', '').length>0?true:false;
  3053. if (apiKey) {
  3054. if (!previousAPIKey){
  3055. resetSettings(true);
  3056. //jank hack to get the UI defaults to load correctly
  3057. }
  3058. GM_setValue('openrouter-api-key', apiKey);
  3059. showStatus('API key saved successfully!');
  3060. fetchAvailableModels(); // Refresh model list
  3061. } else {
  3062. showStatus('Please enter a valid API key');
  3063. }
  3064. }
  3065.  
  3066. /** Clears tweet ratings and updates the relevant UI parts. */
  3067. function clearTweetRatingsAndRefreshUI() {
  3068. if (confirm('Are you sure you want to clear all cached tweet ratings?')) {
  3069. Object.keys(tweetIDRatingCache).forEach(key => delete tweetIDRatingCache[key]);
  3070. GM_setValue('tweetRatings', '{}');
  3071. showStatus('All cached ratings cleared!');
  3072. console.log('Cleared all tweet ratings');
  3073.  
  3074. updateCacheStatsUI();
  3075.  
  3076. // Re-process visible tweets
  3077. if (observedTargetNode) {
  3078. observedTargetNode.querySelectorAll(TWEET_ARTICLE_SELECTOR).forEach(tweet => {
  3079. tweet.dataset.sloppinessScore = ''; // Clear potential old score attribute
  3080. delete tweet.dataset.cachedRating;
  3081. delete tweet.dataset.blacklisted;
  3082. processedTweets.delete(getTweetID(tweet));
  3083. scheduleTweetProcessing(tweet);
  3084. });
  3085. }
  3086. }
  3087. }
  3088.  
  3089. /** Saves the custom instructions from the textarea. */
  3090. function saveInstructions() {
  3091. const instructionsTextarea = document.getElementById('user-instructions');
  3092. USER_DEFINED_INSTRUCTIONS = instructionsTextarea.value;
  3093. GM_setValue('userDefinedInstructions', USER_DEFINED_INSTRUCTIONS);
  3094. showStatus('Scoring instructions saved! New tweets will use these instructions.');
  3095. if (confirm('Do you want to clear the rating cache to apply these instructions to all tweets?')) {
  3096. clearTweetRatingsAndRefreshUI();
  3097. }
  3098. }
  3099.  
  3100. /** Adds a handle from the input field to the blacklist. */
  3101. function addHandleFromInput() {
  3102. const handleInput = document.getElementById('handle-input');
  3103. const handle = handleInput.value.trim();
  3104. if (handle) {
  3105. addHandleToBlacklist(handle);
  3106. handleInput.value = ''; // Clear input after adding
  3107. }
  3108. }
  3109.  
  3110. /**
  3111. * Handles changes to general setting inputs/toggles.
  3112. * @param {HTMLElement} target - The input/toggle element that changed.
  3113. * @param {string} settingName - The name of the setting (from data-setting).
  3114. */
  3115. function handleSettingChange(target, settingName) {
  3116. let value;
  3117. if (target.type === 'checkbox') {
  3118. value = target.checked;
  3119. } else {
  3120. value = target.value;
  3121. }
  3122.  
  3123. // Update global variable if it exists
  3124. if (window[settingName] !== undefined) {
  3125. window[settingName] = value;
  3126. }
  3127.  
  3128. // Save to GM storage
  3129. GM_setValue(settingName, value);
  3130.  
  3131. // Special UI updates for specific settings
  3132. if (settingName === 'enableImageDescriptions') {
  3133. const imageModelContainer = document.getElementById('image-model-container');
  3134. if (imageModelContainer) {
  3135. imageModelContainer.style.display = value ? 'block' : 'none';
  3136. }
  3137. showStatus('Image descriptions ' + (value ? 'enabled' : 'disabled'));
  3138. }
  3139. }
  3140.  
  3141. /**
  3142. * Handles changes to parameter control sliders/number inputs.
  3143. * @param {HTMLElement} target - The slider or number input element.
  3144. * @param {string} paramName - The name of the parameter (from data-param-name).
  3145. */
  3146. function handleParameterChange(target, paramName) {
  3147. const row = target.closest('.parameter-row');
  3148. if (!row) return;
  3149.  
  3150. const slider = row.querySelector('.parameter-slider');
  3151. const valueInput = row.querySelector('.parameter-value');
  3152. const min = parseFloat(slider.min);
  3153. const max = parseFloat(slider.max);
  3154. let newValue = parseFloat(target.value);
  3155.  
  3156. // Clamp value if it's from the number input
  3157. if (target.type === 'number' && !isNaN(newValue)) {
  3158. newValue = Math.max(min, Math.min(max, newValue));
  3159. }
  3160.  
  3161. // Update both slider and input
  3162. if (slider && valueInput) {
  3163. slider.value = newValue;
  3164. valueInput.value = newValue;
  3165. }
  3166.  
  3167. // Update global variable
  3168. if (window[paramName] !== undefined) {
  3169. window[paramName] = newValue;
  3170. }
  3171.  
  3172. // Save to GM storage
  3173. GM_setValue(paramName, newValue);
  3174. }
  3175.  
  3176. /**
  3177. * Handles changes to the main filter slider.
  3178. * @param {HTMLElement} slider - The filter slider element.
  3179. */
  3180. function handleFilterSliderChange(slider) {
  3181. const valueDisplay = document.getElementById('tweet-filter-value');
  3182. currentFilterThreshold = parseInt(slider.value, 10);
  3183. if (valueDisplay) {
  3184. valueDisplay.textContent = currentFilterThreshold.toString();
  3185. }
  3186. GM_setValue('filterThreshold', currentFilterThreshold);
  3187. applyFilteringToAll();
  3188. }
  3189.  
  3190. /**
  3191. * Switches the active tab in the settings panel.
  3192. * @param {string} tabName - The name of the tab to activate (from data-tab).
  3193. */
  3194. function switchTab(tabName) {
  3195. const settingsContent = document.querySelector('#settings-container .settings-content');
  3196. if (!settingsContent) return;
  3197.  
  3198. const tabs = settingsContent.querySelectorAll('.tab-content');
  3199. const buttons = settingsContent.querySelectorAll('.tab-navigation .tab-button');
  3200.  
  3201. tabs.forEach(tab => tab.classList.remove('active'));
  3202. buttons.forEach(btn => btn.classList.remove('active'));
  3203.  
  3204. const tabToShow = settingsContent.querySelector(`#${tabName}-tab`);
  3205. const buttonToActivate = settingsContent.querySelector(`.tab-navigation .tab-button[data-tab="${tabName}"]`);
  3206.  
  3207. if (tabToShow) tabToShow.classList.add('active');
  3208. if (buttonToActivate) buttonToActivate.classList.add('active');
  3209. }
  3210.  
  3211. /**
  3212. * Toggles the visibility of advanced options sections.
  3213. * @param {string} contentId - The ID of the content element to toggle.
  3214. */
  3215. function toggleAdvancedOptions(contentId) {
  3216. const content = document.getElementById(contentId);
  3217. const toggle = document.querySelector(`[data-toggle="${contentId}"]`);
  3218. if (!content || !toggle) return;
  3219.  
  3220. const icon = toggle.querySelector('.advanced-toggle-icon');
  3221. const isExpanded = content.classList.toggle('expanded');
  3222.  
  3223. if (icon) {
  3224. icon.classList.toggle('expanded', isExpanded);
  3225. }
  3226.  
  3227. // Adjust max-height for smooth animation
  3228. if (isExpanded) {
  3229. content.style.maxHeight = content.scrollHeight + 'px';
  3230. } else {
  3231. content.style.maxHeight = '0';
  3232. }
  3233. }
  3234.  
  3235. // --- UI Update Functions ---
  3236.  
  3237. /** Updates the cache statistics display in the General tab. */
  3238. function updateCacheStatsUI() {
  3239. const cachedCountEl = document.getElementById('cached-ratings-count');
  3240. const whitelistedCountEl = document.getElementById('whitelisted-handles-count');
  3241.  
  3242. if (cachedCountEl) {
  3243. cachedCountEl.textContent = Object.keys(tweetIDRatingCache).length;
  3244. }
  3245. if (whitelistedCountEl) {
  3246. whitelistedCountEl.textContent = blacklistedHandles.length;
  3247. }
  3248. }
  3249.  
  3250. /**
  3251. * Refreshes the entire settings UI to reflect current settings.
  3252. */
  3253. function refreshSettingsUI() {
  3254. // Update general settings inputs/toggles
  3255. document.querySelectorAll('[data-setting]').forEach(input => {
  3256. const settingName = input.dataset.setting;
  3257. const value = GM_getValue(settingName, window[settingName]); // Get saved or default value
  3258. if (input.type === 'checkbox') {
  3259. input.checked = value;
  3260. // Trigger change handler for side effects (like hiding/showing image model section)
  3261. handleSettingChange(input, settingName);
  3262. } else {
  3263. input.value = value;
  3264. }
  3265. });
  3266.  
  3267. // Update parameter controls (sliders/number inputs)
  3268. document.querySelectorAll('.parameter-row[data-param-name]').forEach(row => {
  3269. const paramName = row.dataset.paramName;
  3270. const slider = row.querySelector('.parameter-slider');
  3271. const valueInput = row.querySelector('.parameter-value');
  3272. const value = GM_getValue(paramName, window[paramName]);
  3273.  
  3274. if (slider) slider.value = value;
  3275. if (valueInput) valueInput.value = value;
  3276. });
  3277.  
  3278. // Update filter slider
  3279. const filterSlider = document.getElementById('tweet-filter-slider');
  3280. const filterValueDisplay = document.getElementById('tweet-filter-value');
  3281. if (filterSlider && filterValueDisplay) {
  3282. filterSlider.value = currentFilterThreshold.toString();
  3283. filterValueDisplay.textContent = currentFilterThreshold.toString();
  3284. }
  3285.  
  3286. // Refresh dynamically populated lists/dropdowns
  3287. refreshHandleList(document.getElementById('handle-list'));
  3288. refreshModelsUI(); // Refreshes model dropdowns
  3289.  
  3290. // Update cache stats
  3291. updateCacheStatsUI();
  3292.  
  3293. // Set initial state for advanced sections (collapsed by default unless CSS specifies otherwise)
  3294. document.querySelectorAll('.advanced-content').forEach(content => {
  3295. if (!content.classList.contains('expanded')) {
  3296. content.style.maxHeight = '0';
  3297. }
  3298. });
  3299. document.querySelectorAll('.advanced-toggle-icon.expanded').forEach(icon => {
  3300. // Ensure icon matches state if CSS defaults to expanded
  3301. if (!icon.closest('.advanced-toggle')?.nextElementSibling?.classList.contains('expanded')) {
  3302. icon.classList.remove('expanded');
  3303. }
  3304. });
  3305. }
  3306.  
  3307. /**
  3308. * Refreshes the handle list UI.
  3309. * @param {HTMLElement} listElement - The list element to refresh.
  3310. */
  3311. function refreshHandleList(listElement) {
  3312. if (!listElement) return;
  3313.  
  3314. listElement.innerHTML = ''; // Clear existing list
  3315.  
  3316. if (blacklistedHandles.length === 0) {
  3317. const emptyMsg = document.createElement('div');
  3318. emptyMsg.style.cssText = 'padding: 8px; opacity: 0.7; font-style: italic;';
  3319. emptyMsg.textContent = 'No handles added yet';
  3320. listElement.appendChild(emptyMsg);
  3321. return;
  3322. }
  3323.  
  3324. blacklistedHandles.forEach(handle => {
  3325. const item = document.createElement('div');
  3326. item.className = 'handle-item';
  3327.  
  3328. const handleText = document.createElement('div');
  3329. handleText.className = 'handle-text';
  3330. handleText.textContent = '@' + handle;
  3331. item.appendChild(handleText);
  3332.  
  3333. const removeBtn = document.createElement('button');
  3334. removeBtn.className = 'remove-handle';
  3335. removeBtn.textContent = '×';
  3336. removeBtn.title = 'Remove from list';
  3337. // removeBtn listener is handled by delegation in initializeEventListeners
  3338. item.appendChild(removeBtn);
  3339.  
  3340. listElement.appendChild(item);
  3341. });
  3342. }
  3343.  
  3344. /**
  3345. * Updates the model selection dropdowns based on availableModels.
  3346. */
  3347. function refreshModelsUI() {
  3348. const modelSelectContainer = document.getElementById('model-select-container');
  3349. const imageModelSelectContainer = document.getElementById('image-model-select-container');
  3350.  
  3351. const models = availableModels || []; // Ensure availableModels is an array
  3352.  
  3353. // Update main model selector
  3354. if (modelSelectContainer) {
  3355. modelSelectContainer.innerHTML = ''; // Clear current
  3356. createCustomSelect(
  3357. modelSelectContainer,
  3358. 'model-selector', // ID for the custom select element
  3359. models.map(model => ({ value: model.slug || model.id, label: formatModelLabel(model) })),
  3360. selectedModel, // Current selected value
  3361. (newValue) => { // onChange callback
  3362. selectedModel = newValue;
  3363. GM_setValue('selectedModel', selectedModel);
  3364. showStatus('Rating model updated');
  3365. },
  3366. 'Search rating models...' // Placeholder
  3367. );
  3368. }
  3369.  
  3370. // Update image model selector
  3371. if (imageModelSelectContainer) {
  3372. const visionModels = models.filter(model =>
  3373. model.input_modalities?.includes('image') ||
  3374. model.architecture?.input_modalities?.includes('image') ||
  3375. model.architecture?.modality?.includes('image')
  3376. );
  3377.  
  3378. imageModelSelectContainer.innerHTML = ''; // Clear current
  3379. createCustomSelect(
  3380. imageModelSelectContainer,
  3381. 'image-model-selector', // ID for the custom select element
  3382. visionModels.map(model => ({ value: model.slug || model.id, label: formatModelLabel(model) })),
  3383. selectedImageModel, // Current selected value
  3384. (newValue) => { // onChange callback
  3385. selectedImageModel = newValue;
  3386. GM_setValue('selectedImageModel', selectedImageModel);
  3387. showStatus('Image model updated');
  3388. },
  3389. 'Search vision models...' // Placeholder
  3390. );
  3391. }
  3392. }
  3393.  
  3394. /**
  3395. * Formats a model object into a string for display in dropdowns.
  3396. * @param {Object} model - The model object from the API.
  3397. * @returns {string} A formatted label string.
  3398. */
  3399. function formatModelLabel(model) {
  3400. let label = model.slug || model.id || model.name || 'Unknown Model';
  3401. let pricingInfo = '';
  3402.  
  3403. // Extract pricing
  3404. const pricing = model.endpoint?.pricing || model.pricing;
  3405. if (pricing) {
  3406. const promptPrice = parseFloat(pricing.prompt);
  3407. const completionPrice = parseFloat(pricing.completion);
  3408.  
  3409. if (!isNaN(promptPrice)) {
  3410. pricingInfo += ` - $${promptPrice.toFixed(7)}/in`;
  3411. if (!isNaN(completionPrice) && completionPrice !== promptPrice) {
  3412. pricingInfo += ` $${completionPrice.toFixed(7)}/out`;
  3413. }
  3414. } else if (!isNaN(completionPrice)) {
  3415. // Handle case where only completion price is available (less common)
  3416. pricingInfo += ` - $${completionPrice.toFixed(7)}/out`;
  3417. }
  3418. }
  3419.  
  3420. // Add vision icon
  3421. const isVision = model.input_modalities?.includes('image') ||
  3422. model.architecture?.input_modalities?.includes('image') ||
  3423. model.architecture?.modality?.includes('image');
  3424. if (isVision) {
  3425. label = '🖼️ ' + label;
  3426. }
  3427.  
  3428. return label + pricingInfo;
  3429. }
  3430.  
  3431. // --- Custom Select Dropdown Logic (largely unchanged, but included for completeness) ---
  3432.  
  3433. /**
  3434. * Creates a custom select dropdown with search functionality.
  3435. * @param {HTMLElement} container - Container to append the custom select to.
  3436. * @param {string} id - ID for the root custom-select div.
  3437. * @param {Array<{value: string, label: string}>} options - Options for the dropdown.
  3438. * @param {string} initialSelectedValue - Initially selected value.
  3439. * @param {Function} onChange - Callback function when selection changes.
  3440. * @param {string} searchPlaceholder - Placeholder text for the search input.
  3441. */
  3442. function createCustomSelect(container, id, options, initialSelectedValue, onChange, searchPlaceholder) {
  3443. let currentSelectedValue = initialSelectedValue;
  3444.  
  3445. const customSelect = document.createElement('div');
  3446. customSelect.className = 'custom-select';
  3447. customSelect.id = id;
  3448.  
  3449. const selectSelected = document.createElement('div');
  3450. selectSelected.className = 'select-selected';
  3451.  
  3452. const selectItems = document.createElement('div');
  3453. selectItems.className = 'select-items';
  3454. selectItems.style.display = 'none'; // Initially hidden
  3455.  
  3456. const searchField = document.createElement('div');
  3457. searchField.className = 'search-field';
  3458. const searchInput = document.createElement('input');
  3459. searchInput.type = 'text';
  3460. searchInput.className = 'search-input';
  3461. searchInput.placeholder = searchPlaceholder || 'Search...';
  3462. searchField.appendChild(searchInput);
  3463. selectItems.appendChild(searchField);
  3464.  
  3465. // Function to render options
  3466. function renderOptions(filter = '') {
  3467. // Clear previous options (excluding search field)
  3468. while (selectItems.childNodes.length > 1) {
  3469. selectItems.removeChild(selectItems.lastChild);
  3470. }
  3471.  
  3472. const filteredOptions = options.filter(opt =>
  3473. opt.label.toLowerCase().includes(filter.toLowerCase())
  3474. );
  3475.  
  3476. if (filteredOptions.length === 0) {
  3477. const noResults = document.createElement('div');
  3478. noResults.textContent = 'No matches found';
  3479. noResults.style.cssText = 'opacity: 0.7; font-style: italic; padding: 10px; text-align: center; cursor: default;';
  3480. selectItems.appendChild(noResults);
  3481. }
  3482.  
  3483. filteredOptions.forEach(option => {
  3484. const optionDiv = document.createElement('div');
  3485. optionDiv.textContent = option.label;
  3486. optionDiv.dataset.value = option.value;
  3487. if (option.value === currentSelectedValue) {
  3488. optionDiv.classList.add('same-as-selected');
  3489. }
  3490.  
  3491. optionDiv.addEventListener('click', (e) => {
  3492. e.stopPropagation(); // Prevent closing immediately
  3493. currentSelectedValue = option.value;
  3494. selectSelected.textContent = option.label;
  3495. selectItems.style.display = 'none';
  3496. selectSelected.classList.remove('select-arrow-active');
  3497.  
  3498. // Update classes for all items
  3499. selectItems.querySelectorAll('div[data-value]').forEach(div => {
  3500. div.classList.toggle('same-as-selected', div.dataset.value === currentSelectedValue);
  3501. });
  3502.  
  3503. onChange(currentSelectedValue);
  3504. });
  3505. selectItems.appendChild(optionDiv);
  3506. });
  3507. }
  3508.  
  3509. // Set initial display text
  3510. const initialOption = options.find(opt => opt.value === currentSelectedValue);
  3511. selectSelected.textContent = initialOption ? initialOption.label : 'Select an option';
  3512.  
  3513. customSelect.appendChild(selectSelected);
  3514. customSelect.appendChild(selectItems);
  3515. container.appendChild(customSelect);
  3516.  
  3517. // Initial rendering
  3518. renderOptions();
  3519.  
  3520. // Event listeners
  3521. searchInput.addEventListener('input', () => renderOptions(searchInput.value));
  3522. searchInput.addEventListener('click', e => e.stopPropagation()); // Prevent closing
  3523.  
  3524. selectSelected.addEventListener('click', (e) => {
  3525. e.stopPropagation();
  3526. closeAllSelectBoxes(customSelect); // Close others
  3527. const isHidden = selectItems.style.display === 'none';
  3528. selectItems.style.display = isHidden ? 'block' : 'none';
  3529. selectSelected.classList.toggle('select-arrow-active', isHidden);
  3530. if (isHidden) {
  3531. searchInput.focus();
  3532. searchInput.select(); // Select text for easy replacement
  3533. renderOptions(); // Re-render in case options changed
  3534. }
  3535. });
  3536. }
  3537.  
  3538. /** Closes all custom select dropdowns except the one passed in. */
  3539. function closeAllSelectBoxes(exceptThisOne = null) {
  3540. document.querySelectorAll('.custom-select').forEach(select => {
  3541. if (select === exceptThisOne) return;
  3542. const items = select.querySelector('.select-items');
  3543. const selected = select.querySelector('.select-selected');
  3544. if (items) items.style.display = 'none';
  3545. if (selected) selected.classList.remove('select-arrow-active');
  3546. });
  3547. }
  3548.  
  3549. // --- Rating Indicator Logic (Simplified, assuming CSS handles most styling) ---
  3550.  
  3551. /**
  3552. * Updates or creates the rating indicator on a tweet article.
  3553. * @param {Element} tweetArticle - The tweet article element.
  3554. * @param {number|null} score - The numeric rating (null if pending/error).
  3555. * @param {string} status - 'pending', 'rated', 'error', 'cached', 'blacklisted'.
  3556. * @param {string} [description] - Optional description for hover tooltip.
  3557. */
  3558. function setScoreIndicator(tweetArticle, score, status, description = "") {
  3559. let indicator = tweetArticle.querySelector('.score-indicator');
  3560. if (!indicator) {
  3561. indicator = document.createElement('div');
  3562. indicator.className = 'score-indicator';
  3563. tweetArticle.style.position = 'relative'; // Ensure parent is positioned
  3564. tweetArticle.appendChild(indicator);
  3565. // Add hover listeners only once when creating the indicator
  3566. indicator.addEventListener('mouseenter', handleIndicatorMouseEnter);
  3567. indicator.addEventListener('mouseleave', handleIndicatorMouseLeave);
  3568. }
  3569.  
  3570. // Update status class and text content
  3571. indicator.classList.remove('pending-rating', 'rated-rating', 'error-rating', 'cached-rating', 'blacklisted-rating'); // Clear previous
  3572. indicator.dataset.description = description || ''; // Store description
  3573.  
  3574. switch (status) {
  3575. case 'pending':
  3576. indicator.classList.add('pending-rating');
  3577. indicator.textContent = '⏳';
  3578. break;
  3579. case 'error':
  3580. indicator.classList.add('error-rating');
  3581. indicator.textContent = '⚠️';
  3582. break;
  3583. case 'cached':
  3584. indicator.classList.add('cached-rating');
  3585. indicator.textContent = score;
  3586. break;
  3587. case 'blacklisted':
  3588. indicator.classList.add('blacklisted-rating');
  3589. indicator.textContent = score; // Typically 10 for blacklisted
  3590. break;
  3591. case 'rated': // Default/normal rated
  3592. default:
  3593. indicator.classList.add('rated-rating'); // Add a general rated class
  3594. indicator.textContent = score;
  3595. break;
  3596. }
  3597. }
  3598.  
  3599. /** Global tooltip element */
  3600. let scoreTooltip = null;
  3601.  
  3602. /** Creates or gets the shared tooltip element. */
  3603. function getScoreTooltip() {
  3604. if (!scoreTooltip) {
  3605. scoreTooltip = document.createElement('div');
  3606. scoreTooltip.className = 'score-description'; // Use the class from HTML/CSS
  3607. scoreTooltip.style.display = 'none'; // Initially hidden
  3608. scoreTooltip.style.position = 'fixed'; // Use fixed positioning
  3609. scoreTooltip.style.zIndex = '99999999';
  3610. document.body.appendChild(scoreTooltip);
  3611.  
  3612. // Keep tooltip visible when hovering over it
  3613. scoreTooltip.addEventListener('mouseenter', () => {
  3614. scoreTooltip.style.display = 'block';
  3615. });
  3616. scoreTooltip.addEventListener('mouseleave', () => {
  3617. scoreTooltip.style.display = 'none';
  3618. });
  3619. }
  3620. return scoreTooltip;
  3621. }
  3622.  
  3623. /** Formats description text for the tooltip. */
  3624. function formatTooltipDescription(description) {
  3625. if (!description) return '';
  3626. // Basic formatting, can be expanded
  3627. description = description.replace(/\{score:\s*(\d+)\}/g, '<span style="display:inline-block;background-color:#1d9bf0;color:white;padding:3px 10px;border-radius:9999px;margin:8px 0;font-weight:bold;">SCORE: $1</span>');
  3628. description = description.replace(/\n\n/g, '</p><p style="margin-top: 10px;">'); // Smaller margin
  3629. description = description.replace(/\n/g, '<br>');
  3630. return `<p>${description}</p>`;
  3631. }
  3632.  
  3633. /** Handles mouse enter event for score indicators. */
  3634. function handleIndicatorMouseEnter(event) {
  3635. const indicator = event.target;
  3636. const description = indicator.dataset.description;
  3637. if (!description) return;
  3638.  
  3639. const tooltip = getScoreTooltip();
  3640. tooltip.innerHTML = formatTooltipDescription(description);
  3641.  
  3642. // Position the tooltip
  3643. const rect = indicator.getBoundingClientRect();
  3644. const tooltipWidth = tooltip.offsetWidth; // Get width after setting content
  3645. const tooltipHeight = tooltip.offsetHeight;
  3646. const margin = 10;
  3647.  
  3648. let left = rect.right + margin;
  3649. let top = rect.top + (rect.height / 2) - (tooltipHeight / 2);
  3650.  
  3651. // Adjust if going off-screen
  3652. if (left + tooltipWidth > window.innerWidth - margin) {
  3653. left = rect.left - tooltipWidth - margin;
  3654. }
  3655. if (top < margin) {
  3656. top = margin;
  3657. }
  3658. if (top + tooltipHeight > window.innerHeight - margin) {
  3659. top = window.innerHeight - tooltipHeight - margin;
  3660. }
  3661.  
  3662. tooltip.style.left = `${left}px`;
  3663. tooltip.style.top = `${top}px`;
  3664. tooltip.style.display = 'block';
  3665. }
  3666.  
  3667. /** Handles mouse leave event for score indicators. */
  3668. function handleIndicatorMouseLeave() {
  3669. const tooltip = getScoreTooltip();
  3670. // Hide with a slight delay to allow moving cursor to the tooltip
  3671. setTimeout(() => {
  3672. if (tooltip && tooltip.style.display !== 'none' && !tooltip.matches(':hover')) {
  3673. tooltip.style.display = 'none';
  3674. }
  3675. }, 100);
  3676. }
  3677.  
  3678. /** Cleans up the global score tooltip element. */
  3679. function cleanupDescriptionElements() {
  3680. if (scoreTooltip) {
  3681. scoreTooltip.remove();
  3682. scoreTooltip = null;
  3683. }
  3684. }
  3685.  
  3686. // --- Settings Import/Export (Simplified) ---
  3687.  
  3688. /**
  3689. * Exports all settings and cache to a JSON file.
  3690. */
  3691. function exportSettings() {
  3692. try {
  3693. const settingsToExport = {
  3694. apiKey: GM_getValue('openrouter-api-key', ''),
  3695. selectedModel: GM_getValue('selectedModel', 'google/gemini-flash-1.5-8b'),
  3696. selectedImageModel: GM_getValue('selectedImageModel', 'google/gemini-flash-1.5-8b'),
  3697. enableImageDescriptions: GM_getValue('enableImageDescriptions', false),
  3698. modelTemperature: GM_getValue('modelTemperature', 0.5),
  3699. modelTopP: GM_getValue('modelTopP', 0.9),
  3700. imageModelTemperature: GM_getValue('imageModelTemperature', 0.5),
  3701. imageModelTopP: GM_getValue('imageModelTopP', 0.9),
  3702. maxTokens: GM_getValue('maxTokens', 0),
  3703. filterThreshold: GM_getValue('filterThreshold', 1),
  3704. userDefinedInstructions: GM_getValue('userDefinedInstructions', 'Rate the tweet on a scale from 1 to 10 based on its clarity, insight, creativity, and overall quality.'),
  3705. modelSortOrder: GM_getValue('modelSortOrder', 'throughput-high-to-low')
  3706. };
  3707.  
  3708. const data = {
  3709. version: VERSION,
  3710. date: new Date().toISOString(),
  3711. settings: settingsToExport,
  3712. blacklistedHandles: blacklistedHandles || [],
  3713. tweetRatings: tweetIDRatingCache || {}
  3714. };
  3715.  
  3716. const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  3717. const url = URL.createObjectURL(blob);
  3718. const a = document.createElement('a');
  3719. a.href = url;
  3720. a.download = `tweetfilter-ai-backup-${new Date().toISOString().split('T')[0]}.json`;
  3721. a.click();
  3722. URL.revokeObjectURL(url);
  3723. showStatus('Settings exported successfully!');
  3724. } catch (error) {
  3725. console.error('Error exporting settings:', error);
  3726. showStatus('Error exporting settings: ' + error.message);
  3727. }
  3728. }
  3729.  
  3730. /**
  3731. * Imports settings and cache from a JSON file.
  3732. */
  3733. function importSettings() {
  3734. try {
  3735. const input = document.createElement('input');
  3736. input.type = 'file';
  3737. input.accept = '.json';
  3738.  
  3739. input.onchange = (e) => {
  3740. const file = e.target.files[0];
  3741. if (!file) return;
  3742.  
  3743. const reader = new FileReader();
  3744. reader.onload = (event) => {
  3745. try {
  3746. const data = JSON.parse(event.target.result);
  3747. if (!data.settings) throw new Error('Invalid backup file format');
  3748.  
  3749. // Import settings
  3750. for (const key in data.settings) {
  3751. if (window[key] !== undefined) {
  3752. window[key] = data.settings[key];
  3753. }
  3754. GM_setValue(key, data.settings[key]);
  3755. }
  3756.  
  3757. // Import blacklisted handles
  3758. if (data.blacklistedHandles && Array.isArray(data.blacklistedHandles)) {
  3759. blacklistedHandles = data.blacklistedHandles;
  3760. GM_setValue('blacklistedHandles', blacklistedHandles.join('\n'));
  3761. }
  3762.  
  3763. // Import tweet ratings (merge with existing)
  3764. if (data.tweetRatings && typeof data.tweetRatings === 'object') {
  3765. Object.assign(tweetIDRatingCache, data.tweetRatings);
  3766. saveTweetRatings();
  3767. }
  3768.  
  3769. refreshSettingsUI();
  3770. fetchAvailableModels();
  3771. showStatus('Settings imported successfully!');
  3772.  
  3773. } catch (error) {
  3774. console.error('Error parsing settings file:', error);
  3775. showStatus('Error importing settings: ' + error.message);
  3776. }
  3777. };
  3778. reader.readAsText(file);
  3779. };
  3780. input.click();
  3781. } catch (error) {
  3782. console.error('Error importing settings:', error);
  3783. showStatus('Error importing settings: ' + error.message);
  3784. }
  3785. }
  3786.  
  3787. /**
  3788. * Resets all configurable settings to their default values.
  3789. */
  3790. function resetSettings(noconfirm=false) {
  3791. if (noconfirm || confirm('Are you sure you want to reset all settings to their default values? This will not clear your cached ratings or blacklisted handles.')) {
  3792. // Define defaults (should match config.js ideally)
  3793. const defaults = {
  3794. selectedModel: 'google/gemini-flash-1.5-8b',
  3795. selectedImageModel: 'google/gemini-flash-1.5-8b',
  3796. enableImageDescriptions: false,
  3797. modelTemperature: 0.5,
  3798. modelTopP: 0.9,
  3799. imageModelTemperature: 0.5,
  3800. imageModelTopP: 0.9,
  3801. maxTokens: 0,
  3802. filterThreshold: 1,
  3803. userDefinedInstructions: 'Rate the tweet on a scale from 1 to 10 based on its clarity, insight, creativity, and overall quality.',
  3804. modelSortOrder: 'throughput-high-to-low'
  3805. };
  3806.  
  3807. // Apply defaults
  3808. for (const key in defaults) {
  3809. if (window[key] !== undefined) {
  3810. window[key] = defaults[key];
  3811. }
  3812. GM_setValue(key, defaults[key]);
  3813. }
  3814.  
  3815. refreshSettingsUI();
  3816. fetchAvailableModels();
  3817. showStatus('Settings reset to defaults');
  3818. }
  3819. }
  3820.  
  3821. // --- Blacklist/Whitelist Logic ---
  3822.  
  3823. /**
  3824. * Adds a handle to the blacklist, saves, and refreshes the UI.
  3825. * @param {string} handle - The Twitter handle to add (with or without @).
  3826. */
  3827. function addHandleToBlacklist(handle) {
  3828. handle = handle.trim().replace(/^@/, ''); // Clean handle
  3829. if (handle === '' || blacklistedHandles.includes(handle)) {
  3830. showStatus(handle === '' ? 'Handle cannot be empty.' : `@${handle} is already on the list.`);
  3831. return;
  3832. }
  3833. blacklistedHandles.push(handle);
  3834. GM_setValue('blacklistedHandles', blacklistedHandles.join('\n'));
  3835. refreshHandleList(document.getElementById('handle-list'));
  3836. updateCacheStatsUI();
  3837. showStatus(`Added @${handle} to auto-rate list.`);
  3838. }
  3839.  
  3840. /**
  3841. * Removes a handle from the blacklist, saves, and refreshes the UI.
  3842. * @param {string} handle - The Twitter handle to remove (without @).
  3843. */
  3844. function removeHandleFromBlacklist(handle) {
  3845. const index = blacklistedHandles.indexOf(handle);
  3846. if (index > -1) {
  3847. blacklistedHandles.splice(index, 1);
  3848. GM_setValue('blacklistedHandles', blacklistedHandles.join('\n'));
  3849. refreshHandleList(document.getElementById('handle-list'));
  3850. updateCacheStatsUI();
  3851. showStatus(`Removed @${handle} from auto-rate list.`);
  3852. } else {
  3853. console.warn(`Attempted to remove non-existent handle: ${handle}`);
  3854. }
  3855. }
  3856.  
  3857. // --- Initialization ---
  3858.  
  3859. /**
  3860. * Main initialization function for the UI module.
  3861. */
  3862. function initialiseUI() {
  3863. const uiContainer = injectUI();
  3864. if (!uiContainer) return; // Stop if injection failed
  3865.  
  3866. initializeEventListeners(uiContainer);
  3867. refreshSettingsUI(); // Set initial state from saved settings
  3868. fetchAvailableModels(); // Fetch models async
  3869. }
  3870.  
  3871. })();