c.AI Search Sort

Sort search so cards with public definition stays on top and marked with a star

  1. // ==UserScript==
  2. // @name c.AI Search Sort
  3. // @author EnergoStalin
  4. // @description Sort search so cards with public definition stays on top and marked with a star
  5. // @license AGPL-3.0-only
  6. // @version 1.1.1
  7. // @namespace https://c.ai
  8. // @match https://character.ai/*
  9. // @run-at document-body
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=character.ai
  11. // @grant GM.addStyle
  12. // ==/UserScript==
  13.  
  14. (async () => {
  15. var __defProp = Object.defineProperty;
  16. var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
  17.  
  18. // src/util.ts
  19. async function waitNotNull(func, timeout = 1e4, interval = 1e3) {
  20. return new Promise((res, rej) => {
  21. let time = timeout;
  22. const i = setInterval(async () => {
  23. const c = await func();
  24. time -= interval;
  25. if (time <= 0) {
  26. clearInterval(i);
  27. rej();
  28. }
  29. if (!c) return;
  30. clearInterval(i);
  31. res(c);
  32. }, interval);
  33. });
  34. }
  35. __name(waitNotNull, "waitNotNull");
  36. function injectNavigationHook(callback) {
  37. let old = unsafeWindow.location.href;
  38. new MutationObserver(() => {
  39. if (old === unsafeWindow.location.href) return;
  40. old = unsafeWindow.location.href;
  41. callback(old);
  42. }).observe(unsafeWindow.document.body, {
  43. subtree: true,
  44. childList: true
  45. });
  46. callback(old);
  47. }
  48. __name(injectNavigationHook, "injectNavigationHook");
  49.  
  50. // src/api.ts
  51. var pageProps = await waitNotNull(() => document.querySelector("#__NEXT_DATA__")?.textContent).then((e) => JSON.parse(e).props.pageProps);
  52. var token = pageProps.token;
  53. async function getCharacterInfo(id) {
  54. return await fetch(`https://plus.character.ai/chat/character/info/`, {
  55. headers: {
  56. Authorization: `Token ${token}`,
  57. Origin: "https://character.ai/",
  58. Referer: "https://character.ai/",
  59. "Content-Type": "application/json",
  60. Accept: "application/json"
  61. },
  62. method: "POST",
  63. body: JSON.stringify({
  64. external_id: id
  65. })
  66. }).then((e) => e.json()).then((e) => e.character);
  67. }
  68. __name(getCharacterInfo, "getCharacterInfo");
  69.  
  70. // src/styles/tooltip.css
  71. GM.addStyle(`
  72. .tooltip {
  73. position: relative;
  74. cursor: pointer;
  75. }
  76. .tooltip .tooltip-text {
  77. visibility: hidden;
  78. text-align: left;
  79. z-index: 1;
  80. opacity: 0;
  81. transition: opacity 0.3s;
  82. font-size: 0.7em;
  83. color: #a6a6a6;
  84. text-wrap: nowrap;
  85. }
  86. .tooltip .tooltip-head {
  87. text-align: center;
  88. font-size: 1.3em;
  89. color: #a6a6a6;
  90. }
  91. .tooltip .tooltip-even {
  92. flex-basis: 50%;
  93. }
  94. .tooltip .tooltip-number {
  95. color: #b0a676;
  96. text-align: right;
  97. }
  98. .tooltip:hover .tooltip-text {
  99. visibility: visible;
  100. opacity: 1;
  101. }
  102. `);
  103.  
  104. // src/styles/bootstrap.css
  105. GM.addStyle(`
  106. .align-items-start {
  107. align-items: flex-start;
  108. }
  109. `);
  110.  
  111. // src/icons.ts
  112. var starredIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#75FB4C"><path d="M371.01-324 480-390.22 589-324l-29-124 97-84-127-11-50-117-50 117-127 11 96.89 83.95L371.01-324ZM480-72 360-192H192v-168L72-480l120-120v-168h168l120-120 120 120h168v168l120 120-120 120v168H600L480-72Zm0-102 90-90h126v-126l90-90-90-90v-126H570l-90-90-90 90H264v126l-90 90 90 90v126h126l90 90Zm0-306Z"/></svg>';
  113. var pendingIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#5985E1"><path d="M288-420q25 0 42.5-17.5T348-480q0-25-17.5-42.5T288-540q-25 0-42.5 17.5T228-480q0 25 17.5 42.5T288-420Zm192 0q25 0 42.5-17.5T540-480q0-25-17.5-42.5T480-540q-25 0-42.5 17.5T420-480q0 25 17.5 42.5T480-420Zm192 0q25 0 42.5-17.5T732-480q0-25-17.5-42.5T672-540q-25 0-42.5 17.5T612-480q0 25 17.5 42.5T672-420ZM480.28-96Q401-96 331-126t-122.5-82.5Q156-261 126-330.96t-30-149.5Q96-560 126-629.5q30-69.5 82.5-122T330.96-834q69.96-30 149.5-30t149.04 30q69.5 30 122 82.5T834-629.28q30 69.73 30 149Q864-401 834-331t-82.5 122.5Q699-156 629.28-126q-69.73 30-149 30Zm-.28-72q130 0 221-91t91-221q0-130-91-221t-221-91q-130 0-221 91t-91 221q0 130 91 221t221 91Zm0-312Z"/></svg>';
  114.  
  115. // src/statuses.ts
  116. function clearStatus(card) {
  117. card.querySelector("div[data-status]")?.remove();
  118. }
  119. __name(clearStatus, "clearStatus");
  120. function statusWrapper(card, status) {
  121. const d = document.createElement("div");
  122. d.dataset.status = status;
  123. d.classList.add("flex", "flex-col", "tooltip");
  124. card.classList.add("align-items-start");
  125. card.append(d);
  126. return d;
  127. }
  128. __name(statusWrapper, "statusWrapper");
  129. function isStarred(card) {
  130. return Boolean(card.querySelector('div[data-status="starred"]'));
  131. }
  132. __name(isStarred, "isStarred");
  133. function setStarredStatus(card, descriptionLength) {
  134. statusWrapper(card, "starred").innerHTML = `
  135. <div class="flex grow-0 shrink-0 justify-center">
  136. ${starredIcon}
  137. </div>
  138. <div class="flex flex-row gap-1 tooltip-text">
  139. <span class="tooltip-even">Description</span>
  140. <span class="tooltip-even tooltip-number">${descriptionLength}</span>
  141. </div>
  142. `;
  143. }
  144. __name(setStarredStatus, "setStarredStatus");
  145. function setPendingStatus(card) {
  146. statusWrapper(card, "pending").innerHTML = `
  147. <div class="flex flex-row grow-0 shrink-0 w-full items-center justify-center">
  148. ${pendingIcon}
  149. </div>
  150. `;
  151. }
  152. __name(setPendingStatus, "setPendingStatus");
  153.  
  154. // src/sorting/definition.ts
  155. async function _sort(container) {
  156. const nodes = Array.from(container.childNodes);
  157. const promises = nodes.map(async (card) => {
  158. if (isStarred(card)) return [];
  159. setPendingStatus(card);
  160. const info = await getCharacterInfo(card.href.split("/").pop());
  161. clearStatus(card);
  162. if (info.description?.length > 0) {
  163. setStarredStatus(card, info.description.length);
  164. } else {
  165. container.append(card);
  166. }
  167. return [
  168. card,
  169. info.description?.length
  170. ];
  171. });
  172. return Promise.all(promises);
  173. }
  174. __name(_sort, "_sort");
  175. function sortByDefinitionLength(entries, container) {
  176. const sorted = entries.filter(([_, dl]) => dl).sort(([_c1, dl1], [_c2, dl2]) => dl1 > dl2 ? 1 : -1);
  177. for (const [c] of sorted) {
  178. container.insertBefore(c, container.firstChild);
  179. }
  180. }
  181. __name(sortByDefinitionLength, "sortByDefinitionLength");
  182. async function sort(observer, container) {
  183. observer.disconnect();
  184. const entries = await _sort(container);
  185. sortByDefinitionLength(entries, container);
  186. observer.observe(container, {
  187. attributes: false,
  188. childList: true,
  189. subtree: false
  190. });
  191. }
  192. __name(sort, "sort");
  193.  
  194. // src/index.ts
  195. injectNavigationHook(async () => {
  196. console.log(unsafeWindow.location);
  197. if (unsafeWindow.location.pathname !== "/search") return;
  198. const cardsContainer = await waitNotNull(() => document.evaluate("/html/body/div[1]/div/main/div/div/div/main/div/div[2]", document).iterateNext());
  199. const sortSearches = /* @__PURE__ */ __name((_, observer) => sort(observer, cardsContainer), "sortSearches");
  200. sortSearches([], new MutationObserver(sortSearches));
  201. });
  202. })()