Perplexity Model Selection

Adds model selection buttons to Perplexity AI using jQuery

目前为 2025-01-20 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Perplexity Model Selection
  3. // @namespace https://greasyfork.org/en/users/688917
  4. // @version 0.11
  5. // @description Adds model selection buttons to Perplexity AI using jQuery
  6. // @author dpgc, lyh16, mall-fluffy-bongo, RoyRiv3r
  7. // @match https://www.perplexity.ai/*
  8. // @license MIT
  9. // @run-at document-end
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. "use strict";
  14.  
  15. // Check if jQuery is loaded on the page
  16. if (typeof jQuery === "undefined") {
  17. var script = document.createElement("script");
  18. script.src =
  19. "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js";
  20. script.type = "text/javascript";
  21. document.getElementsByTagName("head")[0].appendChild(script);
  22.  
  23. script.onload = function () {
  24. setup();
  25. };
  26. } else {
  27. setup();
  28. }
  29.  
  30. function createModelSelectorElement(buttonText) {
  31. var $button = $("<button/>", {
  32. type: "button",
  33. class:
  34. "model-selector md:hover:bg-offsetPlus text-textOff dark:text-textOffDark md:hover:text-textMain dark:md:hover:bg-offsetPlusDark dark:md:hover:text-textMainDark font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-in-out font-sans select-none items-center relative group/button justify-center text-center items-center rounded-full cursor-point active:scale-95 origin-center whitespace-nowrap inline-flex text-sm px-2 font-medium h-8",
  35. });
  36.  
  37. const $svg = $(`
  38. <svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="bars-filter" class="svg-inline--fa fa-bars-filter fa-fw fa-1x mr-1" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M0 88C0 74.7 10.7 64 24 64H424c13.3 0 24 10.7 24 24s-10.7 24-24 24H24C10.7 112 0 101.3 0 88zM64 248c0-13.3 10.7-24 24-24H360c13.3 0 24 10.7 24 24s-10.7 24-24 24H88c-13.3 0-24-10.7-24-24zM288 408c0 13.3-10.7 24-24 24H184c-13.3 0-24-10.7-24-24s10.7-24 24-24h80c13.3 0 24 10.7 24 24z"></path></svg>
  39. `);
  40. var $textDiv = $(
  41. `<div class="model-selector-text text-align-center relative truncate">${buttonText}</div>`
  42. );
  43. var $buttonContentDiv = $("<div/>", {
  44. class: "flex items-center leading-none justify-center gap-1",
  45. })
  46. .append($svg)
  47. .append($textDiv);
  48.  
  49. $button.append($buttonContentDiv);
  50. var $wrapperDiv = $('<div class="model-selector-wrapper mr-2"/>').append(
  51. $("<span/>").append($button)
  52. );
  53.  
  54. return {
  55. $element: $wrapperDiv,
  56. setModelName: (modelName) => {
  57. // $textDiv.text(`${buttonText} (${modelName})`);
  58. $textDiv.text(`${modelName} `);
  59. },
  60. };
  61. }
  62.  
  63. function createSelectionPopover(sourceElement) {
  64. const createSelectionElement = (input) => {
  65. const { name, onClick } = input;
  66. const $element = $(`
  67. <div class="md:h-full">
  68. <div class="md:h-full">
  69. <div class="relative cursor-pointer md:hover:bg-offsetPlus py-md px-sm md:p-sm rounded md:hover:dark:bg-offsetPlusDark transition-all duration-300 md:h-full -ml-sm md:ml-0 select-none rounded">
  70. <div class="flex items-center justify-between relative">
  71. <div class="flex items-center gap-x-xs default font-sans text-sm font-medium text-textMain dark:text-textMainDark selection:bg-superDuper selection:text-textMain">
  72. <span>${name}</span>
  73. </div>
  74. </div>
  75. </div>
  76. </div>
  77. </div>
  78. `);
  79.  
  80. $element.click(onClick);
  81. return $element;
  82. };
  83.  
  84. const popoverHTML = `<div class="flex justify-center items-center">
  85. <div class="ease-in-out duration-150 transition">
  86. <div class="absolute left-0 top-0 z-30">
  87. <div data-tag="popper" data-popper-reference-hidden="false" data-popper-escaped="false" data-popper-placement="bottom-end" style="position: absolute; inset: 0px 0px auto auto;">
  88. <div class="border animate-in ease-in-out fade-in zoom-in-95 duration-150 rounded shadow-sm p-xs border-borderMain/50 ring-borderMain/50 divide-borderMain/50 dark:divide-borderMainDark/50 dark:ring-borderMainDark/50 dark:border-borderMainDark/50 bg-background dark:bg-backgroundDark">
  89. <div data-tag="menu" class="min-w-[160px] max-w-[250px] border-borderMain/50 ring-borderMain/50 divide-borderMain/50 dark:divide-borderMainDark/50 dark:ring-borderMainDark/50 dark:border-borderMainDark/50 bg-transparent">
  90. <!-- Put elements here -->
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. </div>
  96. </div>`;
  97.  
  98. const $popover = $(popoverHTML);
  99. const $popper = $popover.find('[data-tag="popper"]');
  100. const $menuContaienr = $popover.find('[data-tag="menu"]');
  101.  
  102. if (sourceElement) {
  103. const { top, left, width, height } =
  104. sourceElement.getBoundingClientRect();
  105. const offset = 10;
  106. const popperWidth = $popper.outerWidth();
  107. $popper.css(
  108. "transform",
  109. `translate(${left + (width + popperWidth * 2)}px, ${
  110. top + height + offset
  111. }px)`
  112. );
  113. }
  114.  
  115. return {
  116. $element: $popover,
  117. addSelection: (input) => {
  118. const $selection = createSelectionElement(input);
  119. $menuContaienr.append($selection);
  120. },
  121. };
  122. }
  123.  
  124. async function fetchSettings() {
  125. const url = "https://www.perplexity.ai/p/api/v1/user/settings";
  126. const response = await fetch(url);
  127. if (!response.ok) throw new Error("Failed to fetch settings");
  128. return await response.json();
  129. }
  130.  
  131. function setupSelection() {
  132. let selector = "";
  133. // const currentURL = window.location.href;
  134. // if (currentURL === 'https://www.perplexity.ai/') {
  135. // selector = '.flex.bg-background.dark\\:bg-offsetDark.rounded-l-lg.col-start-1.row-start-2.-ml-2';
  136. // } else if (currentURL.startsWith('https://www.perplexity.ai/search/')) {
  137. // selector = '.pointer-events-none.fixed.z-10.grid-cols-12.gap-xl.px-sm.py-sm.md\\:bottom-lg.md\\:grid.md\\:px-0.bottom-\\[64px\\].border-borderMain\\/50.ring-borderMain\\/50.divide-borderMain\\/50.dark\\:divide-borderMainDark\\/50.dark\\:ring-borderMainDark\\/50.dark\\:border-borderMainDark\\/50.bg-transparent';
  138. // } else {
  139. // return;
  140. // }
  141. selector = ".flex.bg-background.dark\\:bg-offsetDark.rounded-l-lg.col-start-1.row-start-2.-ml-2";
  142.  
  143. const focusAreas = ["Focus", "Academic", "Writing", "Wolfram|Alpha", "YouTube", "Reddit"];
  144. const focusSelectors = focusAreas.map(text => `div:contains("${text}")`).join(', ');
  145. const $focusElement = $(focusSelectors).closest(selector);
  146.  
  147. if (!$focusElement.length) return;
  148.  
  149. if ($focusElement.data("state") === "injected") return;
  150. $focusElement.data("state", "injected");
  151.  
  152. const aiModels = [
  153. {
  154. name: "Default",
  155. code: "turbo",
  156. },
  157. {
  158. name: "Claude 3.5 Sonnet",
  159. code: "claude2",
  160. },
  161. {
  162. name: "Sonar Large",
  163. code: "experimental",
  164. },
  165. {
  166. name: "GPT-4o",
  167. code: "gpt4o",
  168. },
  169. {
  170. name: "Claude 3 Opus",
  171. code: "claude3opus",
  172. },
  173. {
  174. name: "Sonar Huge",
  175. code: "llama_x_large",
  176. },
  177. {
  178. name: "Grok 2",
  179. code: "grok",
  180. },
  181. {
  182. name: "Claude 3.5 Haiku",
  183. code: "claude35haiku",
  184. },
  185. {
  186. name: "O1",
  187. code: "o1",
  188. },
  189. ];
  190.  
  191. const imageModels = [
  192. {
  193. name: "Playground v.3",
  194. code: "default",
  195. },
  196. {
  197. name: "DALL-E 3",
  198. code: "dall-e-3",
  199. },
  200. {
  201. name: "Stable Diffusion XL",
  202. code: "sdxl",
  203. },
  204. {
  205. name: "FLUX.1",
  206. code: "flux",
  207. },
  208. ];
  209.  
  210. const aiModelSelector = createModelSelectorElement("Chat Model");
  211. const imageModelSelector = createModelSelectorElement("Image Model");
  212.  
  213. let latestSettings = undefined;
  214. const getCurrentModel = () => {
  215. return latestSettings?.["default_model"];
  216. };
  217. const getCurrentImageModel = () => {
  218. return latestSettings?.["default_image_generation_model"];
  219. };
  220. const updateFromSettings = () => {
  221. fetchSettings().then((settings) => {
  222. latestSettings = settings;
  223. const aiModelCode = getCurrentModel();
  224. const aiModelName = aiModels.find((m) => m.code === aiModelCode)?.name;
  225. if (aiModelName) aiModelSelector.setModelName(aiModelName);
  226.  
  227. const imageModelCode = getCurrentImageModel();
  228. const imageModelName = imageModels.find(
  229. (m) => m.code === imageModelCode
  230. )?.name;
  231. if (imageModelName) imageModelSelector.setModelName(imageModelName);
  232. });
  233. };
  234. updateFromSettings();
  235.  
  236. const findFiberNodeWithSocket = (fiber) => {
  237. if (!fiber) return null;
  238.  
  239. if (fiber.memoizedProps && fiber.memoizedProps.socket) {
  240. return fiber;
  241. }
  242.  
  243. return (
  244. findFiberNodeWithSocket(fiber.child) ||
  245. findFiberNodeWithSocket(fiber.sibling)
  246. );
  247. };
  248.  
  249. const setModel = async (model, isImageModel) => {
  250. const el = $focusElement[0];
  251. const fiberKey = Object.keys(el).find((k) =>
  252. k.startsWith("__reactFiber")
  253. );
  254. if (!fiberKey) throw new Error("Failed to find key of React Fiber");
  255. const fiber = el[fiberKey];
  256.  
  257. const targetFiber = findFiberNodeWithSocket(fiber);
  258. if (!targetFiber)
  259. throw new Error("Failed to find fiber node with socket property");
  260.  
  261. const settingsKey = isImageModel
  262. ? "default_image_generation_model"
  263. : "default_model";
  264. return await targetFiber.memoizedProps.socket.emitWithAck(
  265. "save_user_settings",
  266. {
  267. [settingsKey]: model,
  268. source: "default",
  269. version: "2.5",
  270. }
  271. );
  272. };
  273.  
  274. aiModelSelector.$element.click(async () => {
  275. const { $element: $popover, addSelection } = createSelectionPopover(
  276. aiModelSelector.$element[0]
  277. );
  278. $("main").append($popover);
  279. const closePopover = () => {
  280. $popover.remove();
  281. $(document).off("click", closePopover);
  282. };
  283. for (const model of aiModels) {
  284. addSelection({
  285. name: model.name,
  286. onClick: async () => {
  287. await setModel(model.code, false);
  288. updateFromSettings();
  289. closePopover();
  290. },
  291. });
  292. }
  293.  
  294. setTimeout(() => {
  295. $(document).on("click", closePopover);
  296. $popover.on("click", (e) => e.stopPropagation());
  297. }, 500);
  298. });
  299.  
  300. imageModelSelector.$element.click(async () => {
  301. const { $element: $popover, addSelection } = createSelectionPopover(
  302. imageModelSelector.$element[0]
  303. );
  304. $("main").append($popover);
  305. const closePopover = () => {
  306. $popover.remove();
  307. $(document).off("click", closePopover);
  308. };
  309. for (const model of imageModels) {
  310. addSelection({
  311. name: model.name,
  312. onClick: async () => {
  313. await setModel(model.code, true);
  314. updateFromSettings();
  315. closePopover();
  316. },
  317. });
  318. }
  319.  
  320. setTimeout(() => {
  321. $(document).on("click", closePopover);
  322. $popover.on("click", (e) => e.stopPropagation());
  323. }, 500);
  324. });
  325.  
  326. $focusElement.append(aiModelSelector.$element);
  327. $focusElement.append(imageModelSelector.$element);
  328.  
  329. // Add CSS styles for responsive layout
  330. $("<style>")
  331. .prop("type", "text/css")
  332. .html(
  333. `
  334. .model-selector-wrapper {
  335. margin-right: 12px; /* Add right margin to create space between buttons */
  336. }
  337. @media (max-width: 768px) {
  338. .model-selector-wrapper {
  339. display: block;
  340. margin-right: 0;
  341. margin-bottom: 8px;
  342. }
  343. .model-selector {
  344. width: 100%;
  345. }
  346. .model-selector-text {
  347. max-width: 120px;
  348. }
  349. }
  350. `
  351. )
  352. .appendTo("head");
  353. }
  354.  
  355. function setup() {
  356. setupSelection();
  357. setInterval(() => {
  358. setupSelection();
  359. console.log("run");
  360. }, 500);
  361. }
  362. })();