Perplexity Model Selection

Adds model selection buttons to Perplexity AI using jQuery

目前为 2024-05-15 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Perplexity Model Selection
  3. // @namespace https://greasyfork.org/en/users/688917
  4. // @version 0.3
  5. // @description Adds model selection buttons to Perplexity AI using jQuery
  6. // @author dpgc, lyh16, mall-fluffy-bongo
  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 = 'https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js';
  19. script.type = 'text/javascript';
  20. document.getElementsByTagName('head')[0].appendChild(script);
  21.  
  22. script.onload = function() {
  23. setup();
  24. };
  25. } else {
  26. setup();
  27. }
  28.  
  29. function createModelSelectorElement(buttonText) {
  30. var $button = $('<button/>', {
  31. type: 'button',
  32. class: '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'
  33. });
  34.  
  35. const $svg = $(`
  36. <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>
  37. `)
  38. var $textDiv = $(`<div class="model-selector-text text-align-center relative truncate">${buttonText}</div>`);
  39. var $buttonContentDiv = $('<div/>', {
  40. class: 'flex items-center leading-none justify-center gap-1',
  41. }).append($svg).append($textDiv);
  42.  
  43. $button.append($buttonContentDiv);
  44. var $wrapperDiv = $('<div class="model-selector-wrapper mr-2"/>').append($('<span/>').append($button));
  45.  
  46. return {
  47. $element: $wrapperDiv,
  48. setModelName: (modelName) => {
  49. // $textDiv.text(`${buttonText} (${modelName})`);
  50. $textDiv.text(`${modelName} `);
  51. }
  52. }
  53. }
  54.  
  55. function createSelectionPopover(sourceElement) {
  56. const createSelectionElement = (input) => {
  57. const {name, onClick} = input;
  58. const $element = $(`
  59. <div class="md:h-full">
  60. <div class="md:h-full">
  61. <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">
  62. <div class="flex items-center justify-between relative">
  63. <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">
  64. <span>${name}</span>
  65. </div>
  66. </div>
  67. </div>
  68. </div>
  69. </div>
  70. `);
  71.  
  72. $element.click(onClick);
  73. return $element;
  74. }
  75.  
  76. const popoverHTML = `<div class="flex justify-center items-center">
  77. <div class="ease-in-out duration-150 transition">
  78. <div class="absolute left-0 top-0 z-30">
  79. <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;">
  80. <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">
  81. <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">
  82. <!-- Put elements here -->
  83. </div>
  84. </div>
  85. </div>
  86. </div>
  87. </div>
  88. </div>`;
  89.  
  90. const $popover = $(popoverHTML);
  91. const $popper = $popover.find('[data-tag="popper"]');
  92. const $menuContaienr = $popover.find('[data-tag="menu"]');
  93.  
  94. if (sourceElement) {
  95. const {top, left, width, height} = sourceElement.getBoundingClientRect();
  96. const offset = 10;
  97. const popperWidth = $popper.outerWidth();
  98. $popper.css('transform', `translate(${left + (width + popperWidth * 2)}px, ${top + height + offset}px)`);
  99. }
  100.  
  101. return {
  102. $element: $popover,
  103. addSelection: (input) => {
  104. const $selection = createSelectionElement(input);
  105. $menuContaienr.append($selection);
  106. }
  107. }
  108.  
  109. }
  110.  
  111. async function fetchSettings() {
  112. const url = 'https://www.perplexity.ai/p/api/v1/user/settings';
  113. const response = await fetch(url);
  114. if (!response.ok) throw new Error('Failed to fetch settings');
  115. return await response.json();
  116. }
  117.  
  118. function setupSelection() {
  119. let selector = '';
  120. // const currentURL = window.location.href;
  121. // if (currentURL === 'https://www.perplexity.ai/') {
  122. // selector = '.flex.bg-background.dark\\:bg-offsetDark.rounded-l-lg.col-start-1.row-start-2.-ml-2';
  123. // } else if (currentURL.startsWith('https://www.perplexity.ai/search/')) {
  124. // 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';
  125. // } else {
  126. // return;
  127. // }
  128. selector = '.flex.bg-background.dark\\:bg-offsetDark.rounded-l-lg.col-start-1.row-start-2.-ml-2';
  129.  
  130. const $focusElement = $('div:contains("Focus")').closest(selector);
  131. if (!$focusElement.length) return;
  132.  
  133. if ($focusElement.data('state') === 'injected') return;
  134. $focusElement.data('state', 'injected');
  135.  
  136. const aiModels = [
  137. {
  138. "name": "Default",
  139. "code": "turbo"
  140. },
  141. {
  142. "name": "Experimental",
  143. "code": "experimental"
  144. },
  145. {
  146. "name": "GPT-4 Turbo",
  147. "code": "gpt4"
  148. },
  149. {
  150. "name": "Claude 3 Opus",
  151. "code": "claude3opus"
  152. },
  153. {
  154. "name": "Claude 3 Sonnet",
  155. "code": "claude2"
  156. },
  157. {
  158. "name": "Mistral Large",
  159. "code": "mistral"
  160. }
  161. ];
  162.  
  163. const imageModels = [
  164. {
  165. "name": "Playground v.2.5",
  166. "code": "default"
  167. },
  168. {
  169. "name": "DALL-E 3",
  170. "code": "dall-e-3"
  171. },
  172. {
  173. "name": "Stable Diffusion XL",
  174. "code": "sdxl"
  175. }
  176. ];
  177.  
  178. const aiModelSelector = createModelSelectorElement("Chat Model");
  179. const imageModelSelector = createModelSelectorElement("Image Model");
  180.  
  181. let latestSettings = undefined;
  182. const getCurrentModel = () => {
  183. return latestSettings?.["default_model"];
  184. }
  185. const getCurrentImageModel = () => {
  186. return latestSettings?.["default_image_generation_model"];
  187. }
  188. const updateFromSettings = () => {
  189. fetchSettings().then((settings) => {
  190. latestSettings = settings;
  191. const aiModelCode = getCurrentModel();
  192. const aiModelName = aiModels.find(m => m.code === aiModelCode)?.name;
  193. if (aiModelName) aiModelSelector.setModelName(aiModelName);
  194.  
  195. const imageModelCode = getCurrentImageModel();
  196. const imageModelName = imageModels.find(m => m.code === imageModelCode)?.name;
  197. if (imageModelName) imageModelSelector.setModelName(imageModelName);
  198. });
  199. };
  200. updateFromSettings();
  201.  
  202. const setModel = async (model, isImageModel) => {
  203. const el = $focusElement[0];
  204. const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber'));
  205. if (!fiberKey) throw new Error('Failed to find key of React Fiber');
  206. const fiber = el[fiberKey];
  207. const settingsKey = isImageModel ? 'default_image_generation_model' : 'default_model';
  208. return await fiber.child.sibling.memoizedProps.socket.emitWithAck('save_user_settings', {
  209. [settingsKey]: model,
  210. source: "default",
  211. version: "2.5"
  212. })
  213. }
  214.  
  215. aiModelSelector.$element.click(async () => {
  216. const {$element: $popover, addSelection} = createSelectionPopover(aiModelSelector.$element[0]);
  217. $('main').append($popover);
  218. const closePopover = () => {
  219. $popover.remove();
  220. $(document).off('click', closePopover);
  221. }
  222. for (const model of aiModels) {
  223. addSelection({
  224. name: model.name,
  225. onClick: async () => {
  226. await setModel(model.code, false);
  227. updateFromSettings();
  228. closePopover();
  229. }
  230. });
  231. }
  232.  
  233. setTimeout(() => {
  234. $(document).on('click', closePopover);
  235. $popover.on('click', (e) => e.stopPropagation());
  236. }, 500);
  237. });
  238.  
  239. imageModelSelector.$element.click(async () => {
  240. const {$element: $popover, addSelection} = createSelectionPopover(imageModelSelector.$element[0]);
  241. $('main').append($popover);
  242. const closePopover = () => {
  243. $popover.remove();
  244. $(document).off('click', closePopover);
  245. }
  246. for (const model of imageModels) {
  247. addSelection({
  248. name: model.name,
  249. onClick: async () => {
  250. await setModel(model.code, true);
  251. updateFromSettings();
  252. closePopover();
  253. }
  254. });
  255. }
  256.  
  257. setTimeout(() => {
  258. $(document).on('click', closePopover);
  259. $popover.on('click', (e) => e.stopPropagation());
  260. }, 500);
  261. });
  262.  
  263. $focusElement.append(aiModelSelector.$element);
  264. $focusElement.append(imageModelSelector.$element);
  265.  
  266. // Add CSS styles for responsive layout
  267. $('<style>')
  268. .prop('type', 'text/css')
  269. .html(`
  270. .model-selector-wrapper {
  271. margin-right: 12px; /* Add right margin to create space between buttons */
  272. }
  273. @media (max-width: 768px) {
  274. .model-selector-wrapper {
  275. display: block;
  276. margin-right: 0;
  277. margin-bottom: 8px;
  278. }
  279. .model-selector {
  280. width: 100%;
  281. }
  282. .model-selector-text {
  283. max-width: 120px;
  284. }
  285. }
  286. `)
  287. .appendTo('head');
  288. }
  289.  
  290. function setup() {
  291. setupSelection();
  292. setInterval(() => {
  293. setupSelection();
  294. console.log('run');
  295. }, 500);
  296. }
  297. })();