// ==UserScript== // @name Perplexity helper // @namespace Tiartyos // @match https://www.perplexity.ai/* // @grant none // @version 7.11 // @author Tiartyos, monnef // @description Simple script that adds buttons to Perplexity website for repeating request using Copilot. // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/lodash-fp/0.10.4/lodash-fp.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js // @require https://cdn.jsdelivr.net/npm/color2k@2.0.2/dist/index.unpkg.umd.js // @require https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jsondiffpatch/0.4.1/jsondiffpatch.umd.min.js // @require https://cdn.jsdelivr.net/npm/hex-to-css-filter@6.0.0/dist/umd/hex-to-css-filter.min.js // @require https://cdn.jsdelivr.net/npm/perplex-plus@0.0.72/dist/lib/perplex-plus.js // @homepageURL https://www.perplexity.ai/ // @license GPL-3.0-or-later // @downloadURL https://update.greasyfork.icu/scripts/469985/Perplexity%20helper.user.js // @updateURL https://update.greasyfork.icu/scripts/469985/Perplexity%20helper.meta.js // ==/UserScript== const PP = window.PP.noConflict(); const jq = PP.jq; const hexToCssFilter = window.HexToCSSFilter.hexToCSSFilter; const $c = (cls, parent) => jq(`.${cls}`, parent); const $i = (id, parent) => jq(`#${id}`, parent); const classNames = (...args) => args.filter(Boolean).join(' '); const takeStr = n => str => str.slice(0, n); const dropStr = n => str => str.slice(n); const dropRightStr = n => str => str.slice(0, -n); const filter = pred => xs => xs.filter(pred); const pipe = x => (...fns) => fns.reduce((acc, fn) => fn(acc), x); const nl = '\n'; const markdownConverter = new showdown.Converter({ tables: true }); let debugMode = false; const enableDebugMode = () => { debugMode = true; }; const userscriptName = 'Perplexity helper'; const logPrefix = `[${userscriptName}]`; const debugLog = (...args) => { if (debugMode) { console.debug(logPrefix, ...args); } }; let debugTags = false; const debugLogTags = (...args) => { if (debugTags) { console.debug(logPrefix, '[tags]', ...args); } }; let debugModalCreation = false; const logModalCreation = (...args) => { if (debugModalCreation) { console.debug(logPrefix, '[modalCreation]', ...args); } } let debugReplaceIconsInMenu = false; const logReplaceIconsInMenu = (...args) => { if (debugReplaceIconsInMenu) { console.debug(logPrefix, '[replaceIconsInMenu]', ...args); } } const log = (...args) => { console.log(logPrefix, ...args); }; const logError = (...args) => { console.error(logPrefix, ...args); }; const enableTagsDebugging = () => { debugTags = true; }; // Throttled debug logging to prevent spam // Stores unique message-parameter combinations for 60 seconds const debugLogThrottleCache = new Map(); const THROTTLE_WINDOW_MS = 60000; // 60 seconds const debugLogThrottled = (message, params = {}) => { if (!debugMode) return; const now = Date.now(); const messageKey = message; // Get or create message cache if (!debugLogThrottleCache.has(messageKey)) { debugLogThrottleCache.set(messageKey, new Map()); } const messageCache = debugLogThrottleCache.get(messageKey); // Create a key for the parameters using deep comparison const paramsKey = JSON.stringify(params, Object.keys(params).sort()); // Check if we've seen this exact combination recently if (messageCache.has(paramsKey)) { const lastLogged = messageCache.get(paramsKey); if (now - lastLogged < THROTTLE_WINDOW_MS) { return; // Skip logging, too recent } } // Log the message and update cache console.debug(logPrefix, '[throttled]', message, params); messageCache.set(paramsKey, now); // Clean up old entries (optional optimization) if (messageCache.size > 100) { // Prevent memory bloat const cutoff = now - THROTTLE_WINDOW_MS; for (const [key, timestamp] of messageCache.entries()) { if (timestamp < cutoff) { messageCache.delete(key); } } } }; ($ => { $.fn.nthParent = function (n) { let $p = $(this); if (!(n > -0)) { return $(); } let p = 1 + n; while (p--) { $p = $p.parent(); } return $p; }; })(jq); const initializePerplexityHelperHandlers = () => { const config = loadConfigOrDefault(); if (!config.toggleModeHooks) { log('toggleModeHooks is disabled, skipping initialization and uninstalling global hook'); PP.uninstallGlobalHook(); return; } // Register the condition checker PP.registerShouldBlockEnterHandler($wrapper => hasActiveToggledTagsForCurrentContext($wrapper)); // Register the handler for blocked enter key PP.registerBlockedEnterHandler(async ($textarea, $wrapper) => { // Flash the textarea indicator $textarea.removeClass(pulseFocusCls); $textarea.get(0).offsetHeight; // Force reflow $textarea.addClass(pulseFocusCls); setTimeout(() => $textarea.removeClass(pulseFocusCls), 400); // Apply toggled tags await applyToggledTagsOnSubmit($wrapper); await PP.sleep(500); // Find and click submit button const $submitBtn = PP.getSubmitButtonAnyExceptMic($wrapper); if ($submitBtn.length) { debugLog('blockedEnterInPromptAreaHandler: Clicking submit button after toggle tags', { $submitBtn, ariaLabel: $submitBtn.attr('aria-label'), dataTestId: $submitBtn.attr('data-testid') }); $submitBtn.click(); } else { debugLog('blockedEnterInPromptAreaHandler: No submit button found'); } }); }; // unpkg had quite often problems, tens of seconds to load, sometime 503 fails // const getLucideIconUrl = iconName => `https://unpkg.com/lucide-static@latest/icons/${iconName}.svg`; const getLucideIconUrl = (iconName) => `https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/${iconName}.svg`; const getBrandIconInfo = (modelName = '', { preferBaseModelCompany = false } = {}) => { const normalizedModelName = modelName.toLowerCase(); // Try to get data from perplex-plus ModelDescriptor first try { const modelDescriptor = PP?.findModelDescriptorByName?.(modelName); if (modelDescriptor) { // Determine which company and color to use based on preferBaseModelCompany setting const useBaseModel = preferBaseModelCompany && modelDescriptor.baseModelCompany; const company = useBaseModel ? modelDescriptor.baseModelCompany : modelDescriptor.company; const companyColor = useBaseModel ? modelDescriptor.baseModelCompanyColor : modelDescriptor.companyColor; // Map company names to icon names const companyToIcon = { 'perplexity': 'perplexity', 'openai': 'openai', 'anthropic': 'claude', 'google': 'gemini', 'xai': 'xai', 'deepseek': 'deepseek', 'meta': 'meta' }; const iconName = companyToIcon[company]; // debugLog('getBrandIconInfo: Found icon for model', { modelName, preferBaseModelCompany, iconName, companyColor, modelDescriptor }); if (iconName && companyColor) { return { iconName, brandColor: companyColor }; } } } catch (error) { // Fallback to original logic if perplex-plus data is not available } debugLogThrottled(`getBrandIconInfo: No icon found for model. modelName = ${modelName}, preferBaseModelCompany = ${preferBaseModelCompany}`); // Original fallback logic if (normalizedModelName.includes('claude')) { return { iconName: 'claude', brandColor: '#D97757' }; } else if (normalizedModelName.includes('gpt') || normalizedModelName.startsWith('o')) { return { iconName: 'openai', brandColor: '#FFFFFF' }; } else if (normalizedModelName.includes('gemini')) { return { iconName: 'gemini', brandColor: '#1C69FF' }; } else if (normalizedModelName.includes('sonar') || normalizedModelName.includes('best') || normalizedModelName.includes('auto')) { return preferBaseModelCompany ? { iconName: 'meta', brandColor: '#1D65C1' } : { iconName: 'perplexity', brandColor: '#22B8CD' }; } else if (normalizedModelName.includes('r1')) { return preferBaseModelCompany ? { iconName: 'deepseek', brandColor: '#4D6BFE' } : { iconName: 'perplexity', brandColor: '#22B8CD' }; } else if (normalizedModelName.includes('grok')) { return { iconName: 'xai', brandColor: '#FFFFFF' }; } else if (normalizedModelName.includes('llama') || normalizedModelName.includes('meta')) { return { iconName: 'meta', brandColor: '#1D65C1' }; } else if (normalizedModelName.includes('anthropic')) { return { iconName: 'anthropic', brandColor: '#F1F0E8' }; } return null; }; const getTDesignIconUrl = iconName => `https://api.iconify.design/tdesign:${iconName}.svg`; const getLobeIconsUrl = iconName => `https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/${iconName}.svg`; const parseIconName = iconName => { if (!iconName.includes(':')) return { typePrefix: 'l', processedIconName: iconName }; const [typePrefix, processedIconName] = iconName.split(':'); return { typePrefix, processedIconName }; }; const getIconUrl = iconName => { const { typePrefix, processedIconName } = parseIconName(iconName); if (typePrefix === 'td') { return getTDesignIconUrl(processedIconName); } if (typePrefix === 'l') { return getLucideIconUrl(processedIconName); } throw new Error(`Unknown icon type: ${typePrefix}`); }; const pplxHelperTag = 'pplx-helper'; const genCssName = x => `${pplxHelperTag}--${x}`; const button = (id, icoName, title, extraClass) => ``; const upperButton = (id, icoName, title) => `
${icoName}
`; const textButton = (id, text, title) => ` `; const icoColor = '#1F1F1F'; const robotIco = ``; const robotRepeatIco = ` `; const cogIco = ` \t `; const perplexityHelperModalId = 'perplexityHelperModal'; const getPerplexityHelperModal = () => $i(perplexityHelperModalId); const modalSettingsTitleCls = genCssName('modal-settings-title'); const gitlabLogo = classes => ` `; const modalLargeIconAnchorClasses = 'hover:scale-110 opacity-50 hover:opacity-100 transition-all duration-300'; const modalTabGroupTabsCls = genCssName('modal-tab-group-tabs'); const modalTabGroupActiveCls = genCssName('modal-tab-group-active'); const modalTabGroupContentCls = genCssName('modal-tab-group-content'); const modalTabGroupSeparatorCls = genCssName('modal-tab-group-separator'); const modalHTML = ` `; const tagsContainerCls = genCssName('tags-container'); const tagContainerCompactCls = genCssName('tag-container-compact'); const tagContainerWiderCls = genCssName('tag-container-wider'); const tagContainerWideCls = genCssName('tag-container-wide'); const tagContainerExtraWideCls = genCssName('tag-container-extra-wide'); const threadTagContainerCls = genCssName('thread-tag-container'); const newTagContainerCls = genCssName('new-tag-container'); const newTagContainerInCollectionCls = genCssName('new-tag-container-in-collection'); const tagCls = genCssName('tag'); const tagDarkTextCls = genCssName('tag-dark-text'); const tagIconCls = genCssName('tag-icon'); const tagPaletteCls = genCssName('tag-palette'); const tagPaletteItemCls = genCssName('tag-palette-item'); const tagTweakNoBorderCls = genCssName('tag-tweak-no-border'); const tagTweakSlimPaddingCls = genCssName('tag-tweak-slim-padding'); const tagsPreviewCls = genCssName('tags-preview'); const tagsPreviewNewCls = genCssName('tags-preview-new'); const tagsPreviewThreadCls = genCssName('tags-preview-thread'); const tagsPreviewNewInCollectionCls = genCssName('tags-preview-new-in-collection'); const tagTweakTextShadowCls = genCssName('tag-tweak-text-shadow'); const tagFenceCls = genCssName('tag-fence'); const tagAllFencesWrapperCls = genCssName('tag-all-fences-wrapper'); const tagRestOfTagsWrapperCls = genCssName('tag-rest-of-tags-wrapper'); const tagFenceContentCls = genCssName('tag-fence-content'); const tagDirectoryCls = genCssName('tag-directory'); const tagDirectoryContentCls = genCssName('tag-directory-content'); const helpTextCls = genCssName('help-text'); const queryBoxCls = genCssName('query-box'); const controlsAreaCls = genCssName('controls-area'); const textAreaCls = genCssName('text-area'); const standardButtonCls = genCssName('standard-button'); const lucideIconParentCls = genCssName('lucide-icon-parent'); const roundedMD = genCssName('rounded-md'); const leftPanelSlimCls = genCssName('left-panel-slim'); const modelIconButtonCls = genCssName('model-icon-button'); const modelLabelCls = genCssName('model-label'); const modelLabelStyleJustTextCls = genCssName('model-label-style-just-text'); const modelLabelStyleButtonSubtleCls = genCssName('model-label-style-button-subtle'); const modelLabelStyleButtonWhiteCls = genCssName('model-label-style-button-white'); const modelLabelStyleButtonCyanCls = genCssName('model-label-style-button-cyan'); const modelLabelOverwriteCyanIconToGrayCls = genCssName('model-label-overwrite-cyan-icon-to-gray'); const modelLabelRemoveCpuIconCls = genCssName('model-label-remove-cpu-icon'); const reasoningModelCls = genCssName('reasoning-model'); const modelLabelLargerIconsCls = genCssName('model-label-larger-icons'); const notReasoningModelCls = genCssName('not-reasoning-model'); const modelIconCls = genCssName('model-icon'); const iconColorCyanCls = genCssName('icon-color-cyan'); const iconColorGrayCls = genCssName('icon-color-gray'); const iconColorWhiteCls = genCssName('icon-color-white'); const iconColorPureWhiteCls = genCssName('icon-color-pure-white'); const errorIconCls = genCssName('error-icon'); const customJsAppliedCls = genCssName('customJsApplied'); const customCssAppliedCls = genCssName('customCssApplied'); const customWidgetsHtmlAppliedCls = genCssName('customWidgetsHtmlApplied'); const sideMenuHiddenCls = genCssName('side-menu-hidden'); const sideMenuLabelsHiddenCls = genCssName('side-menu-labels-hidden'); const topSettingsButtonId = genCssName('settings-button-top'); const leftSettingsButtonId = genCssName('settings-button-left'); const leftSettingsButtonWrapperId = genCssName('settings-button-left-wrapper'); const leftMarginOfThreadContentStylesId = genCssName('left-margin-of-thread-content-styles'); const enhancedSubmitButtonCls = genCssName('enhanced-submit-button'); const enhancedSubmitButtonPhTextCls = genCssName('enhanced-submit-button-ph-text'); const enhancedSubmitButtonActiveCls = genCssName('enhanced-submit-button-active'); const promptAreaKeyListenerCls = genCssName('prompt-area-key-listener'); const promptAreaKeyListenerIndicatorCls = genCssName('prompt-area-key-listener-indicator'); const pulseFocusCls = genCssName('pulse-focus'); const modelSelectionListItemsSemiHideCls = genCssName('model-selection-list-items-semi-hide'); const hideUpgradeToMaxAdsCls = genCssName('hide-upgrade-to-max-ads'); const hideUpgradeToMaxAdsSemiHideCls = genCssName('hide-upgrade-to-max-ads-semi-hide'); const extraSpaceBellowLastAnswerCls = genCssName('extra-space-bellow-last-answer'); const quickProfileButtonCls = genCssName('quick-profile-button'); const quickProfileButtonActiveCls = genCssName('quick-profile-button-active'); const quickProfileButtonDisabledCls = genCssName('quick-profile-button-disabled'); const removeUploadedFilesAllButtonCls = genCssName('remove-uploaded-files-all-button'); const removeUploadedFilesAllButtonWrapperCls = genCssName('remove-uploaded-files-all-button-wrapper'); // Tag editor (experimental) const tagEditorWrapperId = genCssName('tag-editor-wrapper'); const tagEditorOpenButtonId = genCssName('tag-editor-open'); const tagEditorTableCls = genCssName('tag-editor-table'); const tagEditorRowCls = genCssName('tag-editor-row'); const cyanPerplexityColor = '#1fb8cd'; const cyanMediumPerplexityColor = '#204b51'; const cyanDarkPerplexityColor = '#203133'; const cyanVeryDarkPerplexityColor = '#0a2527'; const grayPerplexityColor = '#1f2121'; const grayLightPerplexityColor = '#90908f'; const grayDarkPerplexityColor = '#191a1a'; const extraSpaceBellowLastAnswerContent = ('⸬ '.repeat(6) + '\\A').repeat(2); const styles = ` .textarea_wrapper { display: flex; flex-direction: column; } @import url('https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600&display=swap'); .textarea_wrapper > textarea { width: 100%; background-color: rgba(0, 0, 0, 0.8); padding: 0.4em 0.6em; border-radius: 0.5em; } .textarea_label { } .${helpTextCls} { background-color: #225; padding: 0.3em 0.7em; border-radius: 0.5em; margin: 1em 0; } .${helpTextCls} { cursor: text; } .${helpTextCls} a { text-decoration: underline; } .${helpTextCls} a:hover { color: white; } .${helpTextCls} code { font-size: 80%; background-color: rgba(255, 255, 255, 0.1); border-radius: 0.3em; padding: 0.1em; } .${helpTextCls} pre > code { background: none; } .${helpTextCls} pre { font-size: 80%; overflow: auto; background-color: rgba(255, 255, 255, 0.1); border-radius: 0.3em; padding: 0.1em 1em; } .${helpTextCls} li { list-style: circle; margin-left: 1em; } .${helpTextCls} hr { margin: 1em 0 0.5em 0; border-color: rgba(255, 255, 255, 0.1); } .${helpTextCls} table { border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 0.5em; display: inline-block; } .${helpTextCls} table td, .${helpTextCls} table th { padding: 0.1em 0.5em; } .btn-helper { margin-left: 20px } .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.8) } .modal-content { display: flex; margin: 1em auto; width: calc(100vw - 2em); padding: 20px; border: 1px solid #333; background: linear-gradient(135deg, #151517, #202025); border-radius: 6px; color: rgb(206, 206, 210); flex-direction: column; position: relative; overflow-y: auto; cursor: default; font-family: 'Fira Sans', sans-serif; } .${modalTabGroupTabsCls} { display: flex; flex-direction: row; } .modal-content .${modalTabGroupTabsCls} > button { border-radius: 0.5em 0.5em 0 0; border-bottom: 0; padding: 0.2em 0.5em 0 0.5em; background-color: #1e293b; color: rgba(255, 255, 255, 0.5); outline-bottom: none; white-space: nowrap; } .modal-content .${modalTabGroupTabsCls} > button.${modalTabGroupActiveCls} { /* background-color: #3b82f6; */ color: white; text-shadow: 0 0 1px currentColor; padding: 0.3em 0.5em 0.2em 0.5em; } .modal-content .${modalTabGroupContentCls} { display: flex; flex-direction: column; gap: 1em; padding-top: 1em; } .${modalSettingsTitleCls} { background: linear-gradient(to bottom, white, gray); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold; font-size: 3em; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); user-select: none; margin-top: -0.33em; margin-bottom: -0.33em; } .${modalSettingsTitleCls} .animate-letter { display: inline-block; background: inherit; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; transition: transform 0.3s ease-out; } .${modalSettingsTitleCls} .animate-letter.active { /* Move and highlight on active */ transform: translateY(-10px) rotate(5deg); -webkit-text-fill-color: #4dabff; text-shadow: 0 0 5px #4dabff, 0 0 10px #4dabff; } .modal-content .hover\\:scale-110:hover { transform: scale(1.1); } .modal-content label { padding-right: 10px; } .modal-content hr { height: 1px; margin: 1em 0; border-color: rgba(255, 255, 255, 0.1); } .modal-content hr.${modalTabGroupSeparatorCls} { margin: 0 -1em 0 -1em; } .modal-content input[type="checkbox"] { appearance: none; width: 1.2em; height: 1.2em; border: 2px solid #ffffff80; border-radius: 0.25em; background-color: transparent; transition: all 0.2s ease; cursor: pointer; position: relative; } .modal-content input[type="checkbox"]:checked { background-color: #3b82f6; border-color: #3b82f6; } .modal-content input[type="checkbox"]:checked::after { content: ''; position: absolute; left: 50%; top: 50%; width: 0.4em; height: 0.7em; border: solid white; border-width: 0 2px 2px 0; transform: translate(-50%, -60%) rotate(45deg); } .modal-content input[type="checkbox"]:hover { border-color: #ffffff; } .modal-content input[type="checkbox"]:focus { outline: 2px solid #3b82f680; outline-offset: 2px; } .modal-content .checkbox_label { color: white; line-height: 1.5; } .modal-content .checkbox_wrapper { display: flex; align-items: center; gap: 0.5em; } .modal-content .number_label { margin-left: 0.5em; } .modal-content .color_wrapper { display: flex; align-items: center; } .modal-content .color_label { margin-left: 0.5em; } .modal-content input, .modal-content button { background-color: #1e293b; border: 2px solid #ffffff80; border-radius: 0.5em; color: white; padding: 0.5em; transition: border-color 0.3s ease, outline 0.3s ease; } .modal-content input:hover, .modal-content button:hover { border-color: #ffffff; } .modal-content input:focus, .modal-content button:focus { outline: 2px solid #3b82f680; outline-offset: 2px; } .modal-content input[type="number"] { padding: 0.5em; transition: border-color 0.3s ease, outline 0.3s ease; } .modal-content input[type="color"] { padding: 0; height: 2em; } .modal-content input[type="color"]:hover { border-color: #ffffff; } .modal-content input[type="color"]:focus { outline: 2px solid #3b82f680; outline-offset: 2px; } .modal-content h1 + hr { margin-top: 0.5em; } .modal-content select { appearance: none; background-color: #1e293b; /* Dark blue background */ border: 2px solid #ffffff80; border-radius: 0.5em; padding: 0.3em 2em 0.3em 0.5em; color: white; font-size: 1em; cursor: pointer; transition: all 0.2s ease; background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right 0.5em center; background-size: 1.2em; } .modal-content select option { background-color: #1e293b; /* Match select background */ color: white; padding: 0.5em; } .modal-content select:hover { border-color: #ffffff; } .modal-content select:focus { outline: 2px solid #3b82f680; outline-offset: 2px; } .modal-content .select_label { color: white; margin-left: 0.5em; } .modal-content .select_wrapper { display: flex; align-items: center; } .close { color: rgb(206, 206, 210); float: right; font-size: 28px; font-weight: bold; position: absolute; right: 20px; top: 5px; } .close:hover, .close:focus { color: white; text-decoration: none; cursor: pointer; } #copied-modal,#copied-modal-2 { padding: 5px 5px; background:gray; position:absolute; display: none; color: white; font-size: 15px; } label > div.select-none { user-select: text; cursor: initial; } .${tagsContainerCls} { display: flex; flex-direction: row; margin: 5px 0; } .${tagsContainerCls}.${threadTagContainerCls} { margin-left: 0.5em; margin-right: 0.5em; margin-bottom: 2px; } .${tagContainerCompactCls} { margin-top: -2em; margin-bottom: 1px; } .${tagContainerCompactCls} .${tagFenceCls} { margin: 0; padding: 1px; } .${tagContainerCompactCls} .${tagCls} { } .${tagContainerCompactCls} .${tagAllFencesWrapperCls} { gap: 1px; } .${tagContainerCompactCls} .${tagRestOfTagsWrapperCls} { margin: 1px; } .${tagContainerCompactCls} .${tagRestOfTagsWrapperCls}, .${tagContainerCompactCls} .${tagFenceContentCls}, .${tagContainerCompactCls} .${tagDirectoryContentCls} { gap: 1px; } .${removeUploadedFilesAllButtonWrapperCls} { position: relative; } .${removeUploadedFilesAllButtonCls} { position: absolute; left: 0; top: 0; display: inline-flex; align-items: center; justify-content: center; height: 24px; width: 24px; aspect-ratio: 1 / 1; border-radius: 9999px; background: ${grayPerplexityColor}; opacity: 0.5; color: #9ca3af; cursor: pointer; transition: color 150ms ease, background 150ms ease, transform 150ms ease, opacity 150ms ease; } .${removeUploadedFilesAllButtonCls}:hover { color: #e5e7eb; background: rgba(255,255,255,0.2); opacity: 1; } .${removeUploadedFilesAllButtonCls}:active { transform: scale(0.97); } .${tagContainerWiderCls} { margin-left: -6em; margin-right: -6em; } .${tagContainerWiderCls} .${tagCls} { } .${tagContainerWideCls} { margin-left: -12em; margin-right: -12em; } .${tagContainerExtraWideCls} { margin-left: -16em; margin-right: -16em; max-width: 100vw; } .${tagsContainerCls} { @media (max-width: 768px) { margin-left: 0 !important; margin-right: 0 !important; } } .${tagCls} { border: 1px solid #3b3b3b; background-color: #282828; /*color: rgba(255, 255, 255, 0.482);*/ /* equivalent of #909090; when on #282828 background */ padding: 0px 8px 0 8px; border-radius: 4px; cursor: pointer; transition: background-color 0.2s, color 0.2s; display: inline-block; color: #E8E8E6; user-select: none; } .${tagCls}.${tagDarkTextCls} { color: #171719; } .${tagCls} span { display: inline-block; } .${tagCls}.${tagTweakNoBorderCls} { border: none; } .${tagCls}.${tagTweakSlimPaddingCls} { padding: 0px 4px 0 4px; } .${tagCls} .${tagIconCls} { width: 16px; height: 16px; margin-right: 2px; margin-left: -4px; margin-top: -4px; vertical-align: middle; display: inline-block; filter: invert(1); } .${tagCls}.${tagDarkTextCls} .${tagIconCls} { filter: none; } .${tagCls}.${tagTweakSlimPaddingCls} .${tagIconCls} { margin-left: -2px; } .${tagCls} span { position: relative; top: 1.5px; } .${tagCls}.${tagTweakTextShadowCls} span { text-shadow: 1px 0 0.5px black, -1px 0 0.5px black, 0 1px 0.5px black, 0 -1px 0.5px black; } .${tagCls}.${tagTweakTextShadowCls}.${tagDarkTextCls} span { text-shadow: 1px 0 0.5px white, -1px 0 0.5px white, 0 1px 0.5px white, 0 -1px 0.5px white; } .${tagCls}:hover { background-color: #333; color: #fff; transform: scale(1.02); } .${tagCls}.${tagDarkTextCls}:hover { /* color: #171717; */ color: #2f2f2f; } .${tagCls}:active { transform: scale(0.98); } .${tagPaletteCls} { display: flex; flex-wrap: wrap; gap: 1px; } .${tagPaletteCls} .${tagPaletteItemCls} { text-shadow: 1px 0 1px black, -1px 0 1px black, 0 1px 1px black, 0 -1px 1px black; width: 40px; height: 25px; display: inline-block; text-align: center; padding: 0 2px; transition: color 0.2s, border 0.1s; border: 2px solid transparent; } .${tagPaletteItemCls}:hover { cursor: pointer; color: white; border: 2px solid white; } .${tagsPreviewCls} { background-color: #191a1a; padding: 0.5em 1em; border-radius: 1em; } /* Tag editor styles */ .${tagEditorTableCls} th, .${tagEditorTableCls} td { border-bottom: 1px solid rgba(255,255,255,0.08); padding: 4px; } #${tagEditorWrapperId} { background: rgba(255,255,255,0.02); padding: 8px; border-radius: 8px; } .${tagAllFencesWrapperCls} { display: flex; flex-direction: row; gap: 5px; } .${tagRestOfTagsWrapperCls} { display: flex; flex-direction: row; flex-wrap: wrap; align-content: flex-start; gap: 5px; margin: 8px; } .${tagFenceCls} { display: flex; margin: 5px 0; padding: 5px; border-radius: 4px; } .${tagFenceContentCls} { display: flex; flex-direction: column; flex-wrap: wrap; gap: 5px; } .${tagDirectoryCls} { position: relative; display: flex; z-index: 100; } .${tagDirectoryCls}:hover .${tagDirectoryContentCls} { display: flex; } .${tagDirectoryContentCls} { position: absolute; display: none; flex-direction: column; gap: 5px; top: 0px; padding-bottom: 1px; left: -5px; transform: translateY(-100%); background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 4px; flex-wrap: nowrap; width: max-content; } .${tagDirectoryContentCls} .${tagCls} { white-space: nowrap; width: fit-content; } .${queryBoxCls} { flex-wrap: wrap; } .${controlsAreaCls} { grid-template-columns: repeat(4,minmax(0,1fr)) } .${textAreaCls} { grid-column-end: 5; } .${standardButtonCls} { grid-column-start: 4; } .${roundedMD} { border-radius: 0.375rem!important; } #${leftSettingsButtonId} svg { transition: fill 0.2s; } #${leftSettingsButtonId}:hover svg { fill: #fff !important; } .w-collapsedSideBarWidth #${leftSettingsButtonId} span { display: none; } .w-collapsedSideBarWidth #${leftSettingsButtonId} { width: 100%; border-radius: 0.25rem; height: 40px; } #${leftSettingsButtonWrapperId} { display: flex; padding: 0.1em 0.2em; justify-content: flex-start; } .w-collapsedSideBarWidth #${leftSettingsButtonWrapperId} { justify-content: center; } .${lucideIconParentCls} > img { transition: opacity 0.2s ease; } .${lucideIconParentCls}:hover > img, a.dark\\:text-textMainDark .${lucideIconParentCls} > img { opacity: 1; } .${leftPanelSlimCls} .pt-sm > * { padding: 0.2rem 0 !important; } .${leftPanelSlimCls} { max-width: 45px !important; } .${leftPanelSlimCls} > .py-md { margin-left: -0.1em; } .${leftPanelSlimCls} > .py-md > div.flex-col > * { /* background: red; */ margin-right: 0; max-width: 40px; } .${modelLabelCls} { color: #888; /* padding is from style attr */ transition: color 0.2s, background-color 0.2s, border 0.2s; /* margin-right: 0.5em; margin-left: 0.5em; */ padding-top: 3px; /*margin-right: 0.5em;*/ } button.${modelIconButtonCls} { padding-right: 1.0em; padding-left: 1.0em; gap: 5px; } button:hover > .${modelLabelCls} { color: #fff; } button.${modelIconButtonCls} > .min-w-0 { min-width: 16px; margin-right: 0.0em; } button.${modelLabelRemoveCpuIconCls} { /* margin-left: 0.5em; */ /* padding-left: 0.5em; */ padding-right: 1.25em; } .${modelIconCls} { width: 16px; min-width: 16px; height: 16px; margin-right: 2px; margin-left: 0; margin-top: -0px; opacity: 0.5; transition: opacity 0.2s; } button.${modelLabelLargerIconsCls} .${modelIconCls} { transform: scale(1.2); } button:hover .${modelIconCls} { opacity: 1; } button.${modelLabelRemoveCpuIconCls} .${modelLabelCls} { /*margin-right: 0.5em; */ } button.${modelLabelRemoveCpuIconCls}:has(.${reasoningModelCls}) .${modelLabelCls} { /*margin-right: 0.5em; */ } button.${modelLabelRemoveCpuIconCls}.${notReasoningModelCls} .${modelLabelCls} { /* margin-right: 0.0em; */ } .${modelLabelRemoveCpuIconCls} div:has(div > svg.tabler-icon-cpu) { display: none; } button:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) { border: 1px solid #333; } button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) { background: #333 !important; } /* Apply style even if the span is empty */ button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}:empty) { border: 1px solid #333; } button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}:empty):hover { background: #333 !important; } .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} { color: #8D9191 !important; } button:hover > .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} { color: #fff !important; } .${modelIconButtonCls} svg[stroke] { stroke: #8D9191 !important; } .${modelIconButtonCls}:hover svg[stroke] { stroke: #fff !important; } button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}) { background: #191A1A !important; color: #2D2F2F !important; } button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}):hover { color: #8D9191 !important; } /* Apply style even if the span is empty */ button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}:empty) { background: #191A1A !important; color: #2D2F2F !important; } button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}:empty):hover { color: #8D9191 !important; } .${modelLabelCls}.${modelLabelStyleButtonCyanCls} { color: ${cyanPerplexityColor}; } button:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) { border: 1px solid ${cyanMediumPerplexityColor}; background: ${cyanDarkPerplexityColor} !important; } button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) { border: 1px solid ${cyanPerplexityColor}; } /* Apply style even if the span is empty */ button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}:empty) { border: 1px solid ${cyanMediumPerplexityColor}; background: ${cyanDarkPerplexityColor} !important; } button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}:empty):hover { border: 1px solid ${cyanPerplexityColor}; } .${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) svg[stroke] { stroke: ${cyanPerplexityColor} !important; } .${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}):hover svg[stroke] { stroke: #fff !important; } button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}) { color: #888 !important; } button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}):hover { color: #fff !important; } .${reasoningModelCls} { width: 16px; height: 16px; /* margin-right: 2px; margin-left: 2px; margin-top: -2px; */ filter: invert(); opacity: 0.5; transition: opacity 0.2s; } button.${modelLabelLargerIconsCls} .${reasoningModelCls} { transform: scale(1.2); } button:hover .${reasoningModelCls} { opacity: 1; } div[ph-processed-custom-model-popover] :is(.${modelIconCls}, .${reasoningModelCls}) { opacity: 1; } .${errorIconCls} { width: 16px; height: 16px; margin-right: 4px; margin-left: 4px; margin-top: -0px; opacity: 0.75; transition: opacity 0.2s; } button.${modelLabelLargerIconsCls} .${errorIconCls} { transform: scale(1.2); } button:hover .${errorIconCls} { opacity: 1; } /* button:has(.${reasoningModelCls}) > div > div > svg { width: 32px; height: 16px; margin-left: 8px; margin-right: 12px; margin-top: 0px; min-width: 16px; background-color: cyan; } button:has(.${reasoningModelCls}) > div > div:has(svg) { width: 16px; height: 16px; min-width: 30px; background-color: purple; } */ .${iconColorCyanCls} { filter: invert(54%) sepia(84%) saturate(431%) hue-rotate(139deg) brightness(97%) contrast(90%); transition: filter 0.2s; } button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) .${iconColorCyanCls} { filter: invert(100%); } .${iconColorGrayCls} { filter: invert(100%); opacity: 0.5; transition: filter 0.2s; } button:has(.${reasoningModelCls}):hover .${iconColorGrayCls} { filter: invert(100%); } .${iconColorPureWhiteCls} { filter: invert(100%); } .${iconColorWhiteCls} { filter: invert(50%); transition: filter 0.2s; } button:has(.${reasoningModelCls}):hover .${iconColorWhiteCls} { filter: invert(100%); } .${sideMenuHiddenCls} { display: none; } .${sideMenuLabelsHiddenCls} .p-sm > .font-sans.text-2xs, .${sideMenuLabelsHiddenCls} .min-w-0.pb-sm .font-sans.text-2xs { display: none; } .${enhancedSubmitButtonCls} { position: absolute; top: 0; left: 0; width: 101%; height: 101%; border-radius: inherit; cursor: pointer; background: transparent; box-shadow: 0 0 0 1px transparent; z-index: 10; display: flex; align-items: center; justify-content: center; opacity: 0; transform: scale(1.1); transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); overflow: visible; pointer-events: none; } /* ISSUE: Using hard-coded 'active' class here instead of enhancedSubmitButtonActiveCls */ .${enhancedSubmitButtonCls}.active { opacity: 0.5; transform: scale(1); pointer-events: auto; box-shadow: 0 0 0 1px cyan inset; } .${enhancedSubmitButtonCls}:hover { opacity: 1; background: radial-gradient(circle at right top, rgb(23, 8, 56), rgb(4, 2, 12)); } .${enhancedSubmitButtonCls}::before { content: ''; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; background: transparent; z-index: -1; box-shadow: 0 0 0 1.2px transparent; border-radius: inherit; transition: opacity 0.4s ease-in-out, box-shadow 0.4s ease-in-out; opacity: 0; } /* ISSUE: Using hard-coded 'active' class here instead of enhancedSubmitButtonActiveCls */ .${enhancedSubmitButtonCls}.active::before { opacity: 0.9; box-shadow: 0 0 0 1.2px #00ffff; animation: gradientBorder 3s ease infinite; } .${enhancedSubmitButtonCls}:hover::before { opacity: 1; } @keyframes gradientBorder { 0% { box-shadow: 0 0 0 1.2px rgba(0, 255, 255, 0.6); } 50% { box-shadow: 0 0 0 1.2px rgba(0, 255, 255, 1), 0 0 8px rgba(0, 255, 255, 0.6); } 100% { box-shadow: 0 0 0 1.2px rgba(0, 255, 255, 0.6); } } @keyframes pulseIndicator { 0% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(1.5); opacity: 1; } 100% { transform: scale(1); opacity: 0.6; } } .${enhancedSubmitButtonPhTextCls} { font-family: 'JetBrains Mono', monospace; color: #00c1ff; display: none; position: absolute; font-size: 20px; user-select: none; align-items: center; justify-content: center; width: 100%; height: 100%; } .${enhancedSubmitButtonCls}:hover .${enhancedSubmitButtonPhTextCls} { display: flex; } /* Prompt area with active toggle tags */ textarea.${promptAreaKeyListenerCls}, div[contenteditable].${promptAreaKeyListenerCls} { // @stupid cursor apply model. ${promptAreaKeyListenerCls} <- correct. no bracket! box-shadow: 0 0 0 1px rgba(31, 184, 205, 0.2), 0 0px 0px rgba(31, 184, 205, 0); transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); border-color: rgba(31, 184, 205, 0.2); position: relative; background-image: linear-gradient(to bottom, rgba(31, 184, 205, 0.03), transparent); } /* Nice glow effect when focused */ textarea.${promptAreaKeyListenerCls}:focus, div[contenteditable].${promptAreaKeyListenerCls}:focus { box-shadow: 0 0 0 1px rgba(31, 184, 205, 0.5), 0 0 8px 1px rgba(31, 184, 205, 0.3); border-color: rgba(31, 184, 205, 0.5); background-image: linear-gradient(to bottom, rgba(31, 184, 205, 0.05), transparent); } /* Active indicator for textarea */ .${promptAreaKeyListenerIndicatorCls} { position: absolute; bottom: 5px; right: 5px; width: 4px; height: 4px; border-radius: 50%; background-color: rgba(31, 184, 205, 0.6); z-index: 5; pointer-events: none; box-shadow: 0 0 4px 1px rgba(31, 184, 205, 0.4); animation: pulseIndicator 2s ease-in-out infinite; opacity: 0; transform: scale(0); transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); } /* When actually visible, override initial zero values */ .${promptAreaKeyListenerIndicatorCls}.visible { opacity: 1; transform: scale(1); } /* Pulse focus effect when Enter is pressed */ textarea.${pulseFocusCls}, div[contenteditable].${pulseFocusCls} { // @ stupid cursor apply model. ${pulseFocusCls} <- correct. no bracket! box-shadow: 0 0 0 2px rgba(31, 184, 205, 0.8), 0 0 12px 4px rgba(31, 184, 205, 0.6) !important; border-color: rgba(31, 184, 205, 0.8) !important; transition: none !important; } /* Semi-hide model selection list items with hover effect */ .${modelSelectionListItemsSemiHideCls} { opacity: 0.3; transition: opacity 0.2s ease-in-out; } .${modelSelectionListItemsSemiHideCls}:hover { opacity: 1; } /* Hide upgrade to max ads */ .${hideUpgradeToMaxAdsCls} { display: none !important; } .${hideUpgradeToMaxAdsSemiHideCls} { opacity: 0.3; transition: opacity 0.2s ease-in-out; } .${hideUpgradeToMaxAdsSemiHideCls}:hover { opacity: 1; } .${extraSpaceBellowLastAnswerCls} { padding-bottom: 7.5em; } .${extraSpaceBellowLastAnswerCls}::after { content: '${extraSpaceBellowLastAnswerContent}'; white-space: pre; opacity: 0.02; font-size: 5em; justify-content: center; align-items: top; display: flex; min-height: 1em; margin-bottom: -1em; transition: opacity 5.0s ease-in-out; pointer-events: none; overflow: hidden; } .${extraSpaceBellowLastAnswerCls}:hover::after { opacity: 0.1; } /* Using class, because perplexity historically had multiple prompt areas */ .${quickProfileButtonCls} { box-sizing: border-box; border: 1px solid transparent; background-color: ${grayDarkPerplexityColor}; border-radius: 25%; width: 2.25em; height: 2.2em; display: flex; align-items: center; justify-content: center; cursor: pointer; align-items: center; justify-content: center; transform: scale(1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.1s ease-in-out, transform 0.1s ease-in-out; margin: 0; padding: 0; } .${quickProfileButtonCls}.${quickProfileButtonDisabledCls} { opacity: 0.25; cursor: not-allowed; border-color: ${grayDarkPerplexityColor}; background-color: ${grayDarkPerplexityColor}; } .${quickProfileButtonCls}.${quickProfileButtonActiveCls} { background-color: ${cyanVeryDarkPerplexityColor}; border-color: ${cyanMediumPerplexityColor}; } .${quickProfileButtonCls}.${quickProfileButtonActiveCls} > img { opacity: 0.9; filter: brightness(0) saturate(100%) invert(58%) sepia(59%) saturate(480%) hue-rotate(137deg) brightness(95%) contrast(94%); } .${quickProfileButtonCls}:hover:not(.${quickProfileButtonDisabledCls}) { border-color: ${cyanPerplexityColor}; } .${quickProfileButtonCls}:active:not(.${quickProfileButtonDisabledCls}) { transform: scale(0.97) translateY(1px); box-shadow: 0 0 0 0.5px ${cyanPerplexityColor}; } .${quickProfileButtonCls} > img { width: 1.0em; height: 1.0em; filter: invert(1); opacity: 0.5; transition: opacity 0.2s ease-in-out, filter 0.2s ease-in-out; } .${quickProfileButtonCls}:hover > img { opacity: 1; } `; const TAG_POSITION = { BEFORE: 'before', AFTER: 'after', CARET: 'caret', WRAP: 'wrap', }; const TAG_CONTAINER_TYPE = { NEW: 'new', NEW_IN_COLLECTION: 'new-in-collection', THREAD: 'thread', ALL: 'all', }; const tagsHelpText = ` Each line is one tag. Non-field text is what will be inserted into prompt. Field is denoted by \`<\` and \`>\`, field name is before \`:\`, field value after \`:\`. Supported fields: - \`label\`: tag label shown on tag "box" (new items around prompt input area) - \`position\`: where the tag text will be inserted, default is \`before\`; valid values are \`before\`/\`after\` (existing text) or \`caret\` (at cursor position) or \`wrap\` (wrap text around \`$$wrap$$\` marker) - \`color\`: tag color; CSS colors supported, you can use colors from a pre-generated palette via \`%\` syntax, e.g. \`\`. See palette bellow. - \`tooltip\`: shown on hover (aka title); (default) tooltip can be disabled when this field is set to empty string - \`\` - \`target\`: where the tag will be inserted, default is \`new\`; valid values are \`new\` (on home page or when clicking on "New Thread" button) / \`thread\` (on thread page) / \`all\` (everywhere) - \`hide\`: hide the tag from the tag list - \`link\`: link to a URL, e.g. \`\`, can be used for collections. only one link per tag is supported. - \`link-target\`: target of the link, e.g. \`\` (opens in new tab), default is \`_self\` (same tab). - \`icon\`: Lucide icon name, e.g. \`\`. see [lucide icons](https://lucide.dev/icons). prefix \`td:\` is used for [TDesign icons](https://tdesign.tencent.com/design/icon-en#header-69). prefix \`l:\` for Lucide icons is implicit and can be omitted. - \`toggle-mode\`: makes the tag work as a toggle button. When toggled on (highlighted), a special cyan/green outline appears around the submit button. Click this enhanced submit button to apply all toggled tag actions before submitting. Toggle status is saved between sessions. No parameters needed - just use \`\`. - \`set-mode\`: set the query mode: \`pro\` or \`research\`, e.g. \`\` - \`set-model\`: set the model, e.g. \`\` - \`set-sources\`: set the sources, e.g. \`\` for disabled first source (web), disabled second source (academic), enabled third source (social) - \`auto-submit\`: automatically submit the query after the tag is clicked (applies after other tag actions like \`set-mode\` or \`set-model\`), e.g. \`\` - \`dir\`: unique identifier for a directory tag (it will not insert text into prompt) - \`in-dir\`: identifier of the parent directory this tag belongs to - \`fence\`: unique identifier for a fence definition (hidden by default) - \`in-fence\`: identifier of the fence this tag belongs to - \`fence-width\`: CSS width for a fence, e.g. \`\` - \`fence-border-style\`: CSS border style for a fence (e.g., solid, dashed, dotted) - \`fence-border-color\`: CSS color or a palette \`%\` syntax for a fence border - \`fence-border-width\`: CSS width for a fence border --- | String | Replacement | Example | |---|---|---| | \`\\n\` | newline | | | \`$$time$$\` | current time | \`23:05\` | | \`$$wrap$$\` | sets position where existing text will be inserted | | --- Examples: \`\`\` stable diffusion web ui - , prefer concise modern syntax and style, tell me a joke tell me a joke \n\nNOTE: This is a toggle-mode note appended to the end of prompt \`\`\` Directory example: \`\`\` Games FFXIV: Vintage Story - \`\`\` Fence example: \`\`\` Shounen Seinen Shoujo \`\`\` Another fence example: \`\`\` Haskell Raku \`\`\` `.trim(); const defaultTagColor = '#282828'; const TAGS_PALETTE_COLORS_NUM = 16; const TAGS_PALETTE_CLASSIC = Object.freeze((() => { const step = 360 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor); return _.flow( _.map(x => startH + x * step, _), _.map(h => color2k.hsla(h, startS, startL, startA), _), _.sortBy(x => color2k.parseToHsla(x)[0], _) )(_.range(0, TAGS_PALETTE_COLORS_NUM)); })()); const TAGS_PALETTE_PASTEL = Object.freeze((() => { const step = 360 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor); return _.flow( _.map(x => startH + x * step, _), _.map(h => color2k.hsla(h, startS - 0.2, startL + 0.2, startA), _), _.sortBy(x => color2k.parseToHsla(x)[0], _) )(_.range(0, TAGS_PALETTE_COLORS_NUM)); })()); const TAGS_PALETTE_GRIM = Object.freeze((() => { const step = 360 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor); return _.flow( _.map(x => startH + x * step, _), _.map(h => color2k.hsla(h, startS - 0.6, startL - 0.3, startA), _), _.sortBy(x => color2k.parseToHsla(x)[0], _) )(_.range(0, TAGS_PALETTE_COLORS_NUM)); })()); const TAGS_PALETTE_DARK = Object.freeze((() => { const step = 360 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor); return _.flow( _.map(x => startH + x * step, _), _.map(h => color2k.hsla(h, startS, startL - 0.4, startA), _), _.sortBy(x => color2k.parseToHsla(x)[0], _) )(_.range(0, TAGS_PALETTE_COLORS_NUM)); })()); const TAGS_PALETTE_GRAY = Object.freeze((() => { const step = 1 / TAGS_PALETTE_COLORS_NUM; return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, step * x, 1)); })()); const TAGS_PALETTE_CYAN = Object.freeze((() => { const step = 1 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor); return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(startH, startS, step * x, 1)); })()); const TAGS_PALETTE_TRANSPARENT = Object.freeze((() => { const step = 1 / TAGS_PALETTE_COLORS_NUM; return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, 0, step * x)); })()); const TAGS_PALETTE_HACKER = Object.freeze((() => { const step = 1 / TAGS_PALETTE_COLORS_NUM; return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(120, step * x, step * x * 0.5, 1)); })()); const TAGS_PALETTES = Object.freeze({ CLASSIC: TAGS_PALETTE_CLASSIC, PASTEL: TAGS_PALETTE_PASTEL, GRIM: TAGS_PALETTE_GRIM, DARK: TAGS_PALETTE_DARK, GRAY: TAGS_PALETTE_GRAY, CYAN: TAGS_PALETTE_CYAN, TRANSPARENT: TAGS_PALETTE_TRANSPARENT, HACKER: TAGS_PALETTE_HACKER, CUSTOM: 'CUSTOM', }); const convertColorInPaletteFormat = currentPalette => value => currentPalette[parseInt(dropStr(1)(value), 10)] ?? defaultTagColor; const TAG_HOME_PAGE_LAYOUT = { DEFAULT: 'default', COMPACT: 'compact', WIDER: 'wider', WIDE: 'wide', EXTRA_WIDE: 'extra-wide', }; const parseBinaryState = binaryStr => { if (!/^[01-]+$/.test(binaryStr)) { throw new Error('Invalid binary state: ' + binaryStr); } return binaryStr.split('').map(bit => bit === '1' ? true : bit === '0' ? false : null); }; const processTagField = currentPalette => name => value => { if (name === 'color' && value.startsWith('%')) return convertColorInPaletteFormat(currentPalette)(value); if (name === 'hide') return true; if (name === 'auto-submit') return true; if (name === 'toggle-mode') return true; if (name === 'set-sources') return parseBinaryState(value); return value; }; const tagLineRegex = /<(label|position|color|tooltip|target|hide|link|link-target|icon|dir|in-dir|fence|in-fence|fence-border-style|fence-border-color|fence-border-width|fence-width|set-mode|set-model|auto-submit|set-sources|toggle-mode)(?::([^<>]*))?>/g; const parseOneTagLine = currentPalette => line => Array.from(line.matchAll(tagLineRegex)).reduce( (acc, match) => { const [fullMatch, field, value] = match; const processedValue = processTagField(currentPalette)(field)(value); return { ...acc, [_.camelCase(field)]: processedValue, text: acc.text.replace(fullMatch, '').replace(/\\n/g, '\n'), }; }, { text: line, color: defaultTagColor, target: TAG_CONTAINER_TYPE.NEW, hide: false, 'link-target': '_self', } ); const parseTagsText = text => { const lines = text.split('\n').filter(tag => tag.trim().length > 0); const palette = getPalette(loadConfig()?.tagPalette); return lines.map(parseOneTagLine(palette)).map((x, i) => ({ ...x, originalIndex: i })); }; const getTagsContainer = () => $c(tagsContainerCls); const posFromTag = tag => Object.values(TAG_POSITION).includes(tag.position) ? tag.position : TAG_POSITION.BEFORE; const splitTextAroundWrap = (text) => { const parts = text.split('$$wrap$$'); return { before: parts[0] || '', after: parts[1] || '', }; }; const applyTagToString = (tag, val, caretPos) => { const { text } = tag; const timeString = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); const textAfterTime = text.replace(/\$\$time\$\$/g, timeString); const { before: processedTextBefore, after: processedTextAfter } = splitTextAroundWrap(textAfterTime); const processedText = processedTextBefore; switch (posFromTag(tag)) { case TAG_POSITION.BEFORE: return `${processedText}${val}`; case TAG_POSITION.AFTER: return `${val}${processedText}`; case TAG_POSITION.CARET: return `${takeStr(caretPos)(val)}${processedText}${dropStr(caretPos)(val)}`; case TAG_POSITION.WRAP: return `${processedTextBefore}${val}${processedTextAfter}`; default: throw new Error(`Invalid position: ${tag.position}`); } }; const getPromptAreaFromTagsContainer = tagsContainerEl => PP.getAnyPromptArea(tagsContainerEl.parent()); const getPromptAreaWrapperFromTagsContainer = tagsContainerEl => PP.getAnyPromptAreaWrapper(tagsContainerEl.parent()); const getPalette = paletteName => { // Add this check for 'CUSTOM' if (paletteName === TAGS_PALETTES.CUSTOM) { // Use tagPaletteCustom from config or default if not found return loadConfigOrDefault()?.tagPaletteCustom ?? defaultConfig.tagPaletteCustom; } // Fallback to predefined palettes or CLASSIC as default const palette = TAGS_PALETTES[paletteName]; // Check if palette is an array before returning, otherwise return default return Array.isArray(palette) ? palette : TAGS_PALETTES.CLASSIC; }; // Function to update a toggle tag's visual state const updateToggleTagState = (tagEl, tag, newToggleState) => { if (!tagEl || !tag) return; const isTagLight = color2k.getLuminance(tag.color) > loadConfigOrDefault().tagLuminanceThreshold; const colorMod = isTagLight ? color2k.darken : color2k.lighten; const hoverBgColor = color2k.toRgba(colorMod(tag.color, 0.1)); // For toggle tags, adjust the color based on toggle state const toggledColor = newToggleState ? color2k.lighten(tag.color, 0.3) : tag.color; // Update the tag element tagEl.attr('data-toggled', newToggleState); tagEl.css('background-color', toggledColor); tagEl.attr('data-hoverBgColor', color2k.toHex(hoverBgColor)); // Update tooltip if using default if (!tag.tooltip) { const newTooltip = `${logPrefix} Toggle ${newToggleState ? 'off' : 'on'} - ${tag.label || 'tag'}`; tagEl.prop('title', newTooltip); } }; const createTag = containerEl => isPreview => tag => { if (tag.hide) return null; // Generate a unique identifier for this toggle tag const tagId = generateToggleTagId(tag); // Get saved toggle state if this is a toggle-mode tag and tagToggleSave is enabled const config = loadConfigOrDefault(); // Make sure tagToggledStates exists to prevent errors if (!config.tagToggledStates) { config.tagToggledStates = {}; saveConfig(config); } // TODO: rewrite most of code with _phTagToggleState - new util functions/classes for working with it // Check both the in-memory toggle state and the saved toggle state (if tagToggleSave is enabled) // In-memory toggle state takes precedence during the current session const inMemoryToggleState = window._phTagToggleState && tagId ? window._phTagToggleState[tagId] : undefined; const savedToggleState = (tagId && config.tagToggleSave) ? config.tagToggledStates[tagId] || false : false; const isToggled = inMemoryToggleState !== undefined ? inMemoryToggleState : savedToggleState; const labelString = tag.label ?? tag.text; const isTagLight = color2k.getLuminance(tag.color) > loadConfig().tagLuminanceThreshold; const colorMod = isTagLight ? color2k.darken : color2k.lighten; const hoverBgColor = color2k.toRgba(colorMod(tag.color, 0.1)); const borderColor = color2k.toRgba(colorMod(tag.color, loadConfig().tagTweakRichBorderColor ? 0.2 : 0.1)); const clickHandler = async (evt) => { debugLog('TAG clicked', tag, evt); if (tag.link) return; // Handle toggle mode if (tag.toggleMode) { const el = jq(evt.currentTarget); // Get the current toggle state directly from the element // This is critical for handling multiple clicks correctly const currentToggleState = el.attr('data-toggled') === 'true'; const newToggleState = !currentToggleState; // Update the toggle state in config only if tagToggleSave is enabled // Make sure tagId is valid before using it if (tagId) { const config = loadConfigOrDefault(); // Create a temporary in-memory toggle state for visual indication // We'll track this regardless of tagToggleSave setting window._phTagToggleState = window._phTagToggleState || {}; window._phTagToggleState[tagId] = newToggleState; // Only save the toggle state permanently if the tagToggleSave setting is enabled if (config.tagToggleSave) { const updatedConfig = { ...config, tagToggledStates: { ...config.tagToggledStates, [tagId]: newToggleState } }; saveConfig(updatedConfig); } // Update visual indicators for submit buttons updateToggleIndicators(); // Update the tag's visual state updateToggleTagState(el, tag, newToggleState); } else { debugLog('Error: Invalid toggle tag ID', tag); } return; } // Regular tag handling for non-toggle tags try { // Apply all tag's actions and wait for them to complete await applyTagActions(tag); const $el = jq(evt.currentTarget); // Handle auto submit for this tag after all actions are applied if (tag.autoSubmit) { const $tagsContainer = $el.closest(`.${tagsContainerCls}`); const $promptAreaWrapper = getPromptAreaWrapperFromTagsContainer($tagsContainer); await applyToggledTagsOnSubmit($promptAreaWrapper); const submitButton = PP.getSubmitButtonAnyExceptMic(); debugLog('[createTag] clickHandler: submitButton=', submitButton); if (submitButton.length) { if (submitButton.length > 1) { debugLog('[createTag] clickHandler: multiple submit buttons found, using first one'); } submitButton.first().click(); } else { debugLog('[createTag] clickHandler: no submit button found'); } } else { // Focus the prompt area if we're not auto-submitting const tagsContainer = $el.closest(`.${tagsContainerCls}`); if (tagsContainer.length) { const promptArea = getPromptAreaFromTagsContainer(tagsContainer); if (promptArea.length) { promptArea[0].focus(); } } } } catch (error) { logError('Error applying tag actions:', error); } }; const tagFont = loadConfig().tagFont; // Create tooltip message based on tag type - without using let const tooltipMsg = tag.link ? `${logPrefix} Open link: ${tag.link}` : tag.toggleMode ? `${logPrefix} Toggle ${isToggled ? 'off' : 'on'} - ${tag.label || 'tag'}` : `${logPrefix} Insert \`${tag.text}\` at position \`${posFromTag(tag)}\``; const defaultTooltip = tooltipMsg; // For toggle tags, adjust the color based on toggle state const toggledColor = isToggled ? color2k.lighten(tag.color, 0.3) : tag.color; const backgroundColor = tag.toggleMode ? toggledColor : tag.color; const tagEl = jq(`
`) .addClass(tagCls) .prop('title', tag.tooltip ?? defaultTooltip) .attr('data-tag', JSON.stringify(tag)) .css({ backgroundColor, borderColor, fontFamily: tagFont, borderRadius: `${loadConfig().tagRoundness}px`, }) .attr('data-color', color2k.toHex(tag.color)) .attr('data-hoverBgColor', color2k.toHex(hoverBgColor)) .attr('data-font', tagFont) .attr('data-toggled', isToggled.toString()) .on('mouseenter', event => { jq(event.currentTarget).css('background-color', hoverBgColor); }) .on('mouseleave', event => { const el = jq(event.currentTarget); const isCurrentToggled = el.attr('data-toggled') === 'true'; const currentColor = tag.toggleMode && isCurrentToggled ? color2k.lighten(tag.color, 0.3) : tag.color; el.css('background-color', currentColor); }); if (isTagLight) { tagEl.addClass(tagDarkTextCls); } if (loadConfig()?.tagTweakNoBorder) { tagEl.addClass(tagTweakNoBorderCls); } if (loadConfig()?.tagTweakSlimPadding) { tagEl.addClass(tagTweakSlimPaddingCls); } if (loadConfig()?.tagTweakTextShadow) { tagEl.addClass(tagTweakTextShadowCls); } const textEl = jq('') .text(labelString) .css({ 'font-weight': loadConfig().tagBold ? 'bold' : 'normal', 'font-style': loadConfig().tagItalic ? 'italic' : 'normal', 'font-size': `${loadConfig().tagFontSize}px`, 'transform': `translateY(${loadConfig().tagTextYOffset}px)`, }); if (tag.icon) { const iconEl = jq('') .attr('src', getIconUrl(tag.icon)) .addClass(tagIconCls) .css({ 'width': `${loadConfig().tagIconSize}px`, 'height': `${loadConfig().tagIconSize}px`, 'transform': `translateY(${loadConfig().tagIconYOffset}px)`, }); if (!labelString) { iconEl.css({ marginLeft: '0', marginRight: '0', }); } textEl.prepend(iconEl); } tagEl.append(textEl); if (tag.link) { const linkEl = jq('') .attr('href', tag.link) .attr('target', tag.linkTarget) .css({ textDecoration: 'none', color: 'inherit' }); textEl.wrap(linkEl); } if (!isPreview && !tag.link && !tag.dir) { tagEl.click(clickHandler); } containerEl.append(tagEl); return tagEl; }; const genDebugFakeTags = () => _.times(TAGS_PALETTE_COLORS_NUM, x => `Fake ${x} ${_.times(x / 3).map(() => 'x').join('')}`) .join('\n'); const getTagContainerType = containerEl => { if (containerEl.hasClass(threadTagContainerCls) || containerEl.hasClass(tagsPreviewThreadCls)) return TAG_CONTAINER_TYPE.THREAD; if (containerEl.hasClass(newTagContainerCls) || containerEl.hasClass(tagsPreviewNewCls)) return TAG_CONTAINER_TYPE.NEW; if (containerEl.hasClass(newTagContainerInCollectionCls) || containerEl.hasClass(tagsPreviewNewInCollectionCls)) return TAG_CONTAINER_TYPE.NEW_IN_COLLECTION; return null; }; const getPromptWrapperTagContainerType = promptWrapper => { if (PP.getPromptAreaOfNewThread(promptWrapper).length) return TAG_CONTAINER_TYPE.NEW; if (PP.getPromptAreaOnThread(promptWrapper).length) return TAG_CONTAINER_TYPE.THREAD; if (PP.getPromptAreaOnCollection(promptWrapper).length) return TAG_CONTAINER_TYPE.NEW_IN_COLLECTION; return null; }; const isTagRelevantForContainer = containerType => tag => containerType === tag.target || (containerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION && tag.target === TAG_CONTAINER_TYPE.NEW) || tag.target === TAG_CONTAINER_TYPE.ALL; const tagContainerTypeToTagContainerClass = { [TAG_CONTAINER_TYPE.THREAD]: threadTagContainerCls, [TAG_CONTAINER_TYPE.NEW]: newTagContainerCls, [TAG_CONTAINER_TYPE.NEW_IN_COLLECTION]: newTagContainerInCollectionCls, }; const currentUrlIsSettingsPage = () => window.location.pathname.includes('/settings/'); const refreshTags = ({ force = false } = {}) => { if (!loadConfigOrDefault()?.tagsEnabled) return; const promptWrapper = PP.getPromptAreaWrapperOfNewThread() .add(PP.getPromptAreaWrapperOnThread()) .add(PP.getPromptAreaWrapperOnCollection()) .filter((_, rEl) => { const isPreview = Boolean(jq(rEl).attr('data-preview')); return isPreview || !currentUrlIsSettingsPage(); }); if (!promptWrapper.length) { debugLogTags('no prompt area found'); } // debugLogTags('promptWrappers', promptWrapper); const allTags = _.flow( x => x + (unsafeWindow.phFakeTags ? `${nl}${genDebugFakeTags()}${nl}` : ''), parseTagsText, )(loadConfig()?.tagsText ?? defaultConfig.tagsText); debugLogTags('refreshing allTags', allTags); const createContainer = (promptWrapper) => { const el = jq(`
`).addClass(tagsContainerCls); const tagContainerType = getPromptWrapperTagContainerType(promptWrapper); if (tagContainerType) { const clsToAdd = tagContainerTypeToTagContainerClass[tagContainerType]; if (!clsToAdd) { console.error('Unexpected tagContainerType:', tagContainerType, { promptWrapper }); } el.addClass(clsToAdd); } return el; }; promptWrapper.each((_, rEl) => { const el = jq(rEl); if (el.parent().find(`.${tagsContainerCls}`).length) { el.parent().addClass(queryBoxCls); return; } el.before(createContainer(el)); }); const currentPalette = getPalette(loadConfig().tagPalette); const createFence = (fence) => { const fenceEl = jq('
') .addClass(tagFenceCls) .css({ 'border-style': fence.fenceBorderStyle ?? 'solid', 'border-color': fence.fenceBorderColor?.startsWith('%') ? convertColorInPaletteFormat(currentPalette)(fence.fenceBorderColor) : fence.fenceBorderColor ?? defaultTagColor, 'border-width': fence.fenceBorderWidth ?? '1px', }) .attr('data-tag', JSON.stringify(fence)) ; const fenceContentEl = jq('
') .addClass(tagFenceContentCls) .css({ 'width': fence.fenceWidth ?? '', }) ; fenceEl.append(fenceContentEl); return { fenceEl, fenceContentEl }; }; const createDirectory = () => { const directoryEl = jq('
').addClass(tagDirectoryCls); const directoryContentEl = jq('
').addClass(tagDirectoryContentCls); directoryEl.append(directoryContentEl); return { directoryEl, directoryContentEl }; }; const containerEls = getTagsContainer(); containerEls.each((_i, rEl) => { const containerEl = jq(rEl); const isPreview = Boolean(containerEl.attr('data-preview')); const tagContainerTypeFromPromptWrapper = getPromptWrapperTagContainerType(containerEl.nthParent(2)); const prelimTagContainerType = getTagContainerType(containerEl); if (tagContainerTypeFromPromptWrapper !== prelimTagContainerType && !isPreview) { debugLog('tagContainerTypeFromPromptWrapper !== prelimTagContainerType', { tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview }); containerEl .empty() .removeClass(threadTagContainerCls, newTagContainerCls, newTagContainerInCollectionCls) .addClass(tagContainerTypeToTagContainerClass[tagContainerTypeFromPromptWrapper]) ; } else { if (!isPreview) { debugLogTags('tagContainerTypeFromPromptWrapper === prelimTagContainerType', { tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview }); } } // TODO: use something else than lodash/fp. in following functions it behaved randomly very weirdly // e.g. partial application of map resulting in an empty array or sortBy sorting field name instead // of input array. possibly inconsistent normal FP order of arguments const mapParseAttrTag = xs => xs.map(el => JSON.parse(el.dataset.tag)); const sortByOriginalIndex = xs => [...xs].sort((a, b) => a.originalIndex - b.originalIndex); const tagElsInCurrentContainer = containerEl.find(`.${tagCls}, .${tagFenceCls}`).toArray(); const filterOutHidden = filter(x => !x.hide); const currentTags = _.flow( mapParseAttrTag, sortByOriginalIndex, filterOutHidden, _.uniq, )(tagElsInCurrentContainer); const tagContainerType = getTagContainerType(containerEl); const tagsForThisContainer = _.flow( filter(isTagRelevantForContainer(tagContainerType)), filterOutHidden, sortByOriginalIndex, )(allTags); debugLogTags('tagContainerType =', tagContainerType, ', current tags =', currentTags, ', tagsForThisContainer =', tagsForThisContainer, ', tagElsInCurrentContainer =', tagElsInCurrentContainer); if (_.isEqual(currentTags, tagsForThisContainer) && !force) { debugLogTags('no tags changed'); return; } const diff = jsondiffpatch.diff(currentTags, tagsForThisContainer); const changedTags = jsondiffpatch.formatters.console.format(diff); debugLogTags('changedTags', changedTags); containerEl.empty(); const tagHomePageLayout = loadConfig()?.tagHomePageLayout; if (!isPreview) { if ((tagContainerType === TAG_CONTAINER_TYPE.NEW || tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION)) { if (tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION) { // only compact layout is supported for new in collection if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) { containerEl.addClass(tagContainerCompactCls); } } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) { containerEl.addClass(tagContainerCompactCls); } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDER) { containerEl.addClass(tagContainerWiderCls); } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDE) { containerEl.addClass(tagContainerWideCls); } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.EXTRA_WIDE) { containerEl.addClass(tagContainerExtraWideCls); } else { containerEl.removeClass(`${tagContainerCompactCls} ${tagContainerWiderCls} ${tagContainerWideCls} ${tagContainerExtraWideCls}`); } const extraMargin = loadConfig()?.tagContainerExtraBottomMargin || 0; containerEl.css('margin-bottom', `${extraMargin}em`); } } const fences = {}; const directories = {}; const fencesWrapperEl = jq('
').addClass(tagAllFencesWrapperCls); const restWrapperEl = jq('
').addClass(tagRestOfTagsWrapperCls); tagsForThisContainer.forEach(tag => { const { fence, dir, inFence, inDir } = tag; const getOrCreateDirectory = dirName => { if (!directories[dirName]) directories[dirName] = createDirectory(); return directories[dirName]; }; const getTagContainer = () => { if (fence) { if (!fences[fence]) fences[fence] = createFence(tag); return fences[fence].fenceContentEl; } else if (dir && inFence) { if (!fences[inFence]) { console.error(`fence ${inFence} for tag not found`, tag); return null; } const { directoryEl } = getOrCreateDirectory(dir); fences[inFence].fenceContentEl.append(directoryEl); return directoryEl; } else if (dir) { const { directoryEl } = getOrCreateDirectory(dir); restWrapperEl.append(directoryEl); return directoryEl; } else if (inFence) { if (!fences[inFence]) { console.error(`fence ${inFence} for tag not found`, tag); return null; } return fences[inFence].fenceContentEl; } else if (inDir) { if (!directories[inDir]) { console.error(`directory ${inDir} for tag not found`, tag); return null; } return directories[inDir].directoryContentEl; } else { return restWrapperEl; } }; const tagContainer = getTagContainer(); if (tagContainer && !fence) { createTag(tagContainer)(isPreview)(tag); } }); Object.values(fences).forEach(({ fenceEl }) => fencesWrapperEl.append(fenceEl)); containerEl.append(fencesWrapperEl).append(restWrapperEl); }); }; const setupTags = () => { debugLog('setting up tags'); setInterval(refreshTags, 500); }; const ICON_REPLACEMENT_MODE = Object.freeze({ OFF: 'Off', LUCIDE1: 'Lucide 1', LUCIDE2: 'Lucide 2', LUCIDE3: 'Lucide 3', TDESIGN1: 'TDesign 1', TDESIGN2: 'TDesign 2', TDESIGN3: 'TDesign 3', }); const leftPanelIconMappingsToLucide1 = Object.freeze({ 'search': 'search', 'discover': 'telescope', 'spaces': 'shapes', }); const leftPanelIconMappingsToLucide2 = Object.freeze({ 'search': 'house', 'discover': 'compass', 'spaces': 'square-stack', 'library': 'archive', }); const leftPanelIconMappingsToLucide3 = Object.freeze({ 'search': 'search', 'discover': 'telescope', 'spaces': 'bot', 'library': 'folder-open', }); const leftPanelIconMappingsToTDesign1 = Object.freeze({ 'search': 'search', 'discover': 'compass-filled', 'spaces': 'grid-view', 'library': 'book', }); const leftPanelIconMappingsToTDesign2 = Object.freeze({ 'search': 'search', 'discover': 'shutter-filled', 'spaces': 'palette-1', 'library': 'folder-open-1-filled', }); const leftPanelIconMappingsToTDesign3 = Object.freeze({ 'search': 'search', 'discover': 'banana-filled', 'spaces': 'chili-filled', 'library': 'barbecue-filled', }); const iconMappings = { LUCIDE1: leftPanelIconMappingsToLucide1, LUCIDE2: leftPanelIconMappingsToLucide2, LUCIDE3: leftPanelIconMappingsToLucide3, TDESIGN1: leftPanelIconMappingsToTDesign1, TDESIGN2: leftPanelIconMappingsToTDesign2, TDESIGN3: leftPanelIconMappingsToTDesign3, }; const MODEL_LABEL_TEXT_MODE = Object.freeze({ OFF: 'Off', FULL_NAME: 'Full Name', SHORT_NAME: 'Short Name', PP_MODEL_ID: 'PP Model ID', OWN_NAME_VERSION_SHORT: 'Own Name + Version Short', VERY_SHORT: 'Very Short', FAMILIAR_NAME: 'Familiar Name', }); const MODEL_LABEL_STYLE = Object.freeze({ OFF: 'Off', NO_TEXT: 'No text', JUST_TEXT: 'Just Text', BUTTON_SUBTLE: 'Button Subtle', BUTTON_WHITE: 'Button White', BUTTON_CYAN: 'Button Cyan', }); const CUSTOM_MODEL_POPOVER_MODE = Object.freeze({ OFF: 'Off', SIMPLE_LIST: 'Simple List', COMPACT_LIST: 'Compact List', SIMPLE_GRID: 'Simple 2x Grid', COMPACT_GRID: 'Compact 2x Grid', }); const MODEL_LABEL_ICON_REASONING_MODEL = Object.freeze({ OFF: 'Off', LIGHTBULB: 'Lightbulb', BRAIN: 'Brain', MICROCHIP: 'Microchip', COG: 'Cog', BRAIN_COG: 'Brain Cog', CALCULATOR: 'Calculator', BOT: 'Bot', }); const MODEL_LABEL_ICONS = Object.freeze({ OFF: 'Off', MONOCHROME: 'Monochrome', COLOR: 'Color', }); const MODEL_SELECTION_LIST_ITEMS_MAX_OPTIONS = Object.freeze({ OFF: 'Off', SEMI_HIDE: 'Semi hide', HIDE: 'Hide', }); const HIDE_UPGRADE_TO_MAX_ADS_OPTIONS = Object.freeze({ OFF: 'Off', SEMI_HIDE: 'Semi-Hide', HIDE: 'Hide', }); const defaultConfig = Object.freeze({ // General hideSideMenu: false, slimLeftMenu: false, hideSideMenuLabels: false, hideHomeWidgets: false, hideDiscoverButton: false, hideRelated: false, hideUpgradeToMaxAds: HIDE_UPGRADE_TO_MAX_ADS_OPTIONS.OFF, fixImageGenerationOverlay: false, extraSpaceBellowLastAnswer: false, quickProfileButtonEnabled: false, quickProfiles: [], replaceIconsInMenu: ICON_REPLACEMENT_MODE.OFF, leftMarginOfThreadContent: null, showRemoveAllUploadedFilesButton: true, // Model modelLabelTextMode: MODEL_LABEL_TEXT_MODE.OFF, modelLabelStyle: MODEL_LABEL_STYLE.OFF, modelLabelOverwriteCyanIconToGray: false, modelLabelUseIconForReasoningModels: MODEL_LABEL_ICON_REASONING_MODEL.OFF, modelLabelReasoningModelIconColor: '#ffffff', modelLabelRemoveCpuIcon: false, modelLabelLargerIcons: false, modelLabelIcons: MODEL_LABEL_ICONS.OFF, modelPreferBaseModelIcon: false, customModelPopover: CUSTOM_MODEL_POPOVER_MODE.SIMPLE_GRID, modelIconsInPopover: false, modelSelectionListItemsMax: MODEL_SELECTION_LIST_ITEMS_MAX_OPTIONS.OFF, // Legacy showCopilot: true, showCopilotNewThread: true, showCopilotRepeatLast: true, showCopilotCopyPlaceholder: true, // Tags tagsEnabled: true, tagsText: '', tagPalette: 'CLASSIC', tagPaletteCustom: ['#000', '#fff', '#ff0', '#f00', '#0f0', '#00f', '#0ff', '#f0f'], tagFont: 'Roboto', tagHomePageLayout: TAG_HOME_PAGE_LAYOUT.DEFAULT, tagContainerExtraBottomMargin: 0, tagLuminanceThreshold: 0.35, tagBold: false, tagItalic: false, tagFontSize: 16, tagIconSize: 16, tagRoundness: 4, tagTextYOffset: 0, tagIconYOffset: 0, tagToggleSave: false, toggleModeHooks: true, tagToggleModeIndicators: true, tagToggledStates: {}, // Store toggle states by tag identifier // Raw mainCaptionHtml: '', mainCaptionHtmlEnabled: false, customJs: '', customJsEnabled: false, customCss: '', customCssEnabled: false, customWidgetsHtml: '', customWidgetsHtmlEnabled: false, // Settings activeSettingsTab: 'general', // Debug debugMode: false, debugTagsMode: false, debugTagsSuppressSubmit: false, autoOpenSettings: false, debugModalCreation: false, debugEventHooks: false, debugReplaceIconsInMenu: false, leftMarginOfThreadContentEnabled: false, leftMarginOfThreadContent: 0, }); // TODO: if still using local storage, at least it should be prefixed with user script name const storageKey = 'checkBoxStates'; const loadConfig = () => { try { // TODO: use storage from GM API const val = JSON.parse(localStorage.getItem(storageKey)); // debugLog('loaded config', val); return val; } catch (e) { console.error('Failed to load config, using default', e); return defaultConfig; } }; const loadConfigOrDefault = () => loadConfig() ?? defaultConfig; const saveConfig = cfg => { debugLog('saving config', cfg); localStorage.setItem(storageKey, JSON.stringify(cfg)); }; const createCheckbox = (id, labelText, onChange) => { logModalCreation('Creating checkbox', { id, labelText }); const checkbox = jq(``); const label = jq(``); const checkboxWithLabel = jq('
').append(checkbox).append(' ').append(label); logModalCreation('checkboxwithlabel', checkboxWithLabel); getSettingsLastTabGroupContent().append(checkboxWithLabel); checkbox.on('change', onChange); return checkbox; }; const createTextArea = (id, labelText, onChange, helpText, links) => { logModalCreation('Creating text area', { id, labelText }); const textarea = jq(``); const bookIconHtml = ``; const labelTextHtml = `${labelText}`; const label = jq(``); const labelWithLinks = jq('
').addClass('flex flex-row gap-2 mb-2').append(label); const textareaWrapper = jq('
').append(labelWithLinks); if (links) { links.forEach(({ icon, label, url, tooltip }) => { const iconHtml = ``; const link = jq(`${icon ? iconHtml : ''}${label ? ' ' + label : ''}`); link.attr('title', tooltip); labelWithLinks.append(link); }); } if (helpText) { const help = jq(`
`).addClass(helpTextCls).html(markdownConverter.makeHtml(helpText)).append(jq('
')); help.find('a').each((_, a) => jq(a).attr('target', '_blank')); help.append(jq('