', {
'class': 'size-[90%] rounded-md duration-150 [grid-area:1/-1] group-hover:opacity-100 opacity-0 border-borderMain/50 ring-borderMain/50 divide-borderMain/50 dark:divide-borderMainDark/50 dark:ring-borderMainDark/50 dark:border-borderMainDark/50 bg-offsetPlus dark:bg-offsetPlusDark'
}),
jq(svgHtml).addClass('relative duration-150 [grid-area:1/-1] group-hover:scale-110 text-text-200'),
),
jq('
', {
'class': 'font-sans text-2xs md:text-xs text-textOff dark:text-textOffDark selection:bg-super/50 selection:text-textMain dark:selection:bg-superDuper/10 dark:selection:text-superDark',
'text': label ?? 'MISSING LABEL'
})
);
};
const handleLeftSettingsButtonSetup = () => {
const existingLeftSettingsButton = getLeftSettingsButtonEl();
if (existingLeftSettingsButton.length === 1) {
// const wrapper = existingLeftSettingsButton.parent();
// if (!wrapper.is(':last-child')) {
// wrapper.appendTo(wrapper.parent());
// }
return;
}
const $leftPanel = PP.getIconsInLeftPanel();
if ($leftPanel.length === 0) {
debugLog('handleLeftSettingsButtonSetup: leftPanel not found');
}
const $sidebarButton = createSidebarButton({
svgHtml: cogIco,
label: 'Perplexity Helper',
testId: 'perplexity-helper-settings',
href: '#',
})
.attr('id', leftSettingsButtonId)
.on('click', () => {
debugLog('left settings button clicked');
if (!PP.isBreakpoint('md')) {
PP.getLeftPanel().hide();
}
showPerplexityHelperSettingsModal();
});
$leftPanel.append($sidebarButton);
};
const handleSlimLeftMenu = () => {
const config = loadConfigOrDefault();
if (!config.slimLeftMenu) return;
const $leftPanel = PP.getLeftPanel();
if ($leftPanel.length === 0) {
// debugLog('handleSlimLeftMenu: leftPanel not found');
}
$leftPanel.addClass(leftPanelSlimCls);
$leftPanel.find('.py-md').css('width', '45px');
};
const handleHideHomeWidgets = () => {
const config = loadConfigOrDefault();
if (!config.hideHomeWidgets) return;
const homeWidgets = PP.getHomeWidgets();
if (homeWidgets.length === 0) {
debugLog('handleHideHomeWidgets: homeWidgets not found');
return;
}
if (homeWidgets.length > 1) {
console.warn(logPrefix, '[handleHideHomeWidgets] too many homeWidgets found', homeWidgets);
}
homeWidgets.hide();
};
const handleFixImageGenerationOverlay = () => {
const config = loadConfigOrDefault();
if (!config.fixImageGenerationOverlay) return;
const imageGenerationOverlay = PP.getImageGenerationOverlay();
if (imageGenerationOverlay.length === 0) {
// debugLog('handleFixImageGenerationOverlay: imageGenerationOverlay not found');
return;
}
// only if wrench button is cyan (we are in custom prompt)
if (!imageGenerationOverlay.find('button').hasClass('bg-super')) return;
const transform = imageGenerationOverlay.css('transform');
if (!transform) return;
// Handle both matrix and translate formats
const matrixMatch = transform.match(/matrix\(.*,\s*([\d.]+),\s*([\d.]+)\)/);
const translateMatch = transform.match(/translate\(([\d.]+)px(?:,\s*([\d.]+)px)?\)/);
const currentX = matrixMatch
? matrixMatch[1] // Matrix format: 5th value is X translation
: translateMatch?.[1] || 0; // Translate format: first value
debugLog('[handleFixImageGenerationOverlay] currentX', currentX, 'transform', transform);
imageGenerationOverlay.css({
transform: `translate(${currentX}px, 0px)`
});
};
const handleExtraSpaceBellowLastAnswer = () => {
const config = loadConfigOrDefault();
if (!config.extraSpaceBellowLastAnswer) return;
jq('body')
.find(`.erp-sidecar\\:h-fit .md\\:pt-md.isolate > .max-w-threadContentWidth`)
.last()
.css({
// backgroundColor: 'magenta',
paddingBottom: '15em',
})
;
};
const handleSearchPage = () => {
const controlsArea = getCurrentControlsArea();
controlsArea.addClass(controlsAreaCls);
controlsArea.parent().find('textarea').first().addClass(textAreaCls);
controlsArea.addClass(roundedMD);
controlsArea.parent().addClass(roundedMD);
if (controlsArea.length === 0) {
debugLog('controlsArea not found', {
controlsArea,
currentControlsArea: getCurrentControlsArea(),
isStandardControlsAreaFc: isStandardControlsAreaFc()
});
}
const lastQueryBoxText = getLastQuery();
const mainTextArea = isStandardControlsAreaFc() ? controlsArea.prev().prev() : controlsArea.parent().prev();
if (mainTextArea.length === 0) {
debugLog('mainTextArea not found', mainTextArea);
}
debugLog('lastQueryBoxText', { lastQueryBoxText });
if (lastQueryBoxText) {
const copilotNewThread = getCopilotNewThreadButton();
const copilotRepeatLast = getCopilotRepeatLastButton();
if (controlsArea.length > 0 && copilotNewThread.length < 1) {
controlsArea.append(button('copilot_new_thread', robotIco, "Starts new thread for with last query text and Copilot ON", standardButtonCls));
}
// Due to updates in Perplexity, this is unnecessary for now
// if (controlsArea.length > 0 && copilotRepeatLast.length < 1) {
// controlsArea.append(button('copilot_repeat_last', robotRepeatIco, "Repeats last query with Copilot ON"));
// }
if (!copilotNewThread.attr('data-has-custom-click-event')) {
copilotNewThread.on("click", function () {
debugLog('copilotNewThread Button clicked!');
openNewThreadModal(getLastQuery());
});
copilotNewThread.attr('data-has-custom-click-event', true);
}
if (!copilotRepeatLast.attr('data-has-custom-click-event')) {
copilotRepeatLast.on("click", function () {
const controlsArea = getCurrentControlsArea();
const textAreaElement = controlsArea.parent().find('textarea')[0];
const coPilotRepeatLastAutoSubmit =
getSavedStates()
? getSavedStates().coPilotRepeatLastAutoSubmit
: getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked');
debugLog('coPilotRepeatLastAutoSubmit', coPilotRepeatLastAutoSubmit);
changeValueUsingEvent(textAreaElement, getLastQuery());
const copilotToggleButton = getCopilotToggleButton(mainTextArea);
debugLog('mainTextArea', mainTextArea);
debugLog('copilotToggleButton', copilotToggleButton);
toggleBtnDot(copilotToggleButton, true);
const isCopilotOnBtn = () => isCopilotOn(copilotToggleButton);
const copilotCheck = () => {
const ctx = { timer: null };
ctx.timer = setInterval(() => checkForCopilotToggleState(ctx.timer, isCopilotOnBtn, coPilotRepeatLastAutoSubmit, 0), 500);
};
copilotCheck();
debugLog('copilot_repeat_last Button clicked!');
});
copilotRepeatLast.attr('data-has-custom-click-event', true);
}
}
if (getNumberOfDashedSVGs() > 0 && getNumberOfDashedSVGs() === getDashedCheckboxButton().length
&& getSelectAllButton().length < 1 && getSelectAllAndSubmitButton().length < 1) {
debugLog('getNumberOfDashedSVGs() === getNumberOfDashedSVGs()', getNumberOfDashedSVGs());
debugLog('getSpecifyQuestionBox', getSpecifyQuestionBox());
const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper();
debugLog('specifyQuestionControlsWrapper', specifyQuestionControlsWrapper);
const selectAllButton = textButton('perplexity_helper_select_all', 'Select all', 'Selects all options');
const selectAllAndSubmitButton = textButton('perplexity_helper_select_all_and_submit', 'Select all & submit', 'Selects all options and submits');
specifyQuestionControlsWrapper.append(selectAllButton);
specifyQuestionControlsWrapper.append(selectAllAndSubmitButton);
getSelectAllButton().on("click", function () {
selectAllCheckboxes();
});
getSelectAllAndSubmitButton().on("click", function () {
selectAllCheckboxes();
setTimeout(() => {
getSpecifyQuestionControlsWrapper().find('button:contains("Continue")').click();
}, 200);
});
}
const constructClipBoard = (buttonId, buttonGetter, modalGetter, copiedModalId, elementGetter) => {
const placeholderValue = getSpecifyQuestionBox().find('textarea').attr('placeholder');
const clipboardInstance = new ClipboardJS(`#${buttonId}`, {
text: () => placeholderValue
});
const copiedModal = `
Copied!`;
debugLog('copiedModalId', copiedModalId);
debugLog('copiedModal', copiedModal);
jq('main').append(copiedModal);
clipboardInstance.on('success', _ => {
var buttonPosition = buttonGetter().position();
jq(`#${copiedModalId}`).css({
top: buttonPosition.top - 30,
left: buttonPosition.left + 50
}).show();
if (elementGetter !== undefined) {
changeValueUsingEvent(elementGetter()[0], placeholderValue);
}
setTimeout(() => {
modalGetter().hide();
}, 5000);
});
};
if (questionBoxWithPlaceholderExists() && getCopyPlaceholder().length < 1) {
const copyPlaceholder = textButton('perplexity_helper_copy_placeholder', 'Copy placeholder', 'Copies placeholder value');
const copyPlaceholderAndFillIn = textButton('perplexity_helper_copy_placeholder_and_fill_in', 'Copy placeholder and fill in',
'Copies placeholder value and fills in input');
const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper();
specifyQuestionControlsWrapper.append(copyPlaceholder);
specifyQuestionControlsWrapper.append(copyPlaceholderAndFillIn);
constructClipBoard('perplexity_helper_copy_placeholder', getCopyPlaceholder, getCopiedModal, 'copied-modal');
constructClipBoard('perplexity_helper_copy_placeholder_and_fill_in', getCopyAndFillInPlaceholder, getCopiedModal2, 'copied-modal-2', getCopyPlaceholderInput);
}
};
const getLabelFromModelDescription = modelLabelStyle => modelLabelFromAriaLabel => modelDescription => {
if (!modelDescription) return modelLabelFromAriaLabel;
switch (modelLabelStyle) {
case MODEL_LABEL_TEXT_MODE.OFF:
return '';
case MODEL_LABEL_TEXT_MODE.FULL_NAME:
return modelDescription.nameEn;
case MODEL_LABEL_TEXT_MODE.SHORT_NAME:
return modelDescription.nameEnShort ?? modelDescription.nameEn;
case MODEL_LABEL_TEXT_MODE.PP_MODEL_ID:
return modelDescription.ppModelId;
case MODEL_LABEL_TEXT_MODE.OWN_NAME_VERSION_SHORT:
const nameText = modelDescription.ownNameEn ?? modelDescription.nameEn;
const versionTextRaw = modelDescription.ownVersionEnShort ?? modelDescription.ownVersionEn;
const versionText = versionTextRaw?.replace(/ P$/, ' Pro'); // HACK: Gemini 2.5 Pro
return [nameText, versionText].filter(Boolean).join(modelDescription.ownNameVersionSeparator ?? ' ');
case MODEL_LABEL_TEXT_MODE.VERY_SHORT:
const abbr = modelDescription.abbrEn;
if (!abbr) {
console.warn('[getLabelFromModelDescription] modelDescription.abbrEn is empty', modelDescription);
} else {
return abbr;
}
const shortName = modelDescription.nameEnShort ?? modelDescription.nameEn;
return shortName.split(/\s+/).map(word => word.charAt(0)).join('');
default:
throw new Error(`Unknown model label style: ${modelLabelStyle}`);
}
};
const getExtraClassesFromModelLabelStyle = modelLabelStyle => {
switch (modelLabelStyle) {
case MODEL_LABEL_STYLE.BUTTON_SUBTLE:
return modelLabelStyleButtonSubtleCls;
case MODEL_LABEL_STYLE.BUTTON_WHITE:
return modelLabelStyleButtonWhiteCls;
case MODEL_LABEL_STYLE.BUTTON_CYAN:
return modelLabelStyleButtonCyanCls;
case MODEL_LABEL_STYLE.NO_TEXT:
return '';
default:
return '';
}
};
const handleModelLabel = () => {
const config = loadConfigOrDefault();
if (!config.modelLabelStyle || config.modelLabelStyle === MODEL_LABEL_STYLE.OFF) return;
const $modelIcons = PP.getAnyModelButton();
$modelIcons.each((_, el) => {
const $el = jq(el);
// Initial setup if elements don't exist yet
if (!$el.find(`.${modelLabelCls}`).length) {
$el.prepend(jq(`
`));
$el.closest('.col-start-3').removeClass('col-start-3').addClass('col-start-2 col-end-4');
}
if (!$el.hasClass(modelIconButtonCls)) {
$el.addClass(modelIconButtonCls);
}
// Get current config state and model information
const modelDescription = PP.getModelDescriptionFromModelButton($el);
const modelLabelFromAriaLabel = $el.attr('aria-label');
const modelLabel = config.modelLabelStyle === MODEL_LABEL_STYLE.NO_TEXT ? '' :
getLabelFromModelDescription(config.modelLabelTextMode)(modelLabelFromAriaLabel)(modelDescription);
if (modelLabel === undefined || modelLabel === null) {
console.error('[handleModelLabel] modelLabel is empty', { modelDescription, modelLabelFromAriaLabel, $el });
return;
}
// Calculate the style classes
const extraClasses = [
getExtraClassesFromModelLabelStyle(config.modelLabelStyle),
config.modelLabelOverwriteCyanIconToGray ? modelLabelOverwriteCyanIconToGrayCls : '',
].filter(Boolean).join(' ');
// Check the current "CPU icon removal" configuration state
const shouldRemoveCpuIcon = config.modelLabelRemoveCpuIcon;
const hasCpuIconRemoval = $el.hasClass(modelLabelRemoveCpuIconCls);
// Only update CPU icon removal class if needed
if (shouldRemoveCpuIcon !== hasCpuIconRemoval) {
if (shouldRemoveCpuIcon) {
$el.addClass(modelLabelRemoveCpuIconCls);
} else {
$el.removeClass(modelLabelRemoveCpuIconCls);
}
}
// Handle larger icons setting
const shouldUseLargerIcons = config.modelLabelLargerIcons;
const hasLargerIconsClass = $el.hasClass(modelLabelLargerIconsCls);
// Only update larger icons class if needed
if (shouldUseLargerIcons !== hasLargerIconsClass) {
if (shouldUseLargerIcons) {
$el.addClass(modelLabelLargerIconsCls);
} else {
$el.removeClass(modelLabelLargerIconsCls);
}
}
// Work with the label element
const $label = $el.find(`.${modelLabelCls}`);
// Use data attributes to track current state
const storedModelDescriptionStr = $label.attr('data-model-description');
const storedExtraClasses = $label.attr('data-extra-classes');
const storedLabel = $label.attr('data-label-text');
// Only update if something has changed
const modelDescriptionStr = JSON.stringify(modelDescription);
const needsUpdate =
storedModelDescriptionStr !== modelDescriptionStr ||
storedExtraClasses !== extraClasses ||
storedLabel !== modelLabel;
if (needsUpdate) {
// Store the current state in data attributes
$label.attr('data-model-description', modelDescriptionStr);
$label.attr('data-extra-classes', extraClasses);
$label.attr('data-label-text', modelLabel);
// Apply the text content
$label.text(modelLabel);
// Apply classes only if they've changed
if (storedExtraClasses !== extraClasses) {
$label.removeClass(modelLabelStyleButtonSubtleCls)
.removeClass(modelLabelStyleButtonWhiteCls)
.removeClass(modelLabelStyleButtonCyanCls)
.removeClass(modelLabelOverwriteCyanIconToGrayCls)
.addClass(extraClasses);
}
}
// Handle error icon if errorType exists
const hasErrorType = modelDescription?.errorType !== undefined;
const existingErrorIcon = $el.find(`.${errorIconCls}`);
// Check if we need to add or remove the error icon
if (hasErrorType && existingErrorIcon.length === 0) {
// Add the error icon
const errorIconUrl = getLucideIconUrl('alert-triangle');
const $errorIcon = jq(`

`)
.attr('data-error-type', modelDescription.errorType)
.css('filter', hexToCssFilter('#FFA500').filter)
.attr('title', modelDescription.errorString || 'Error: Used fallback model');
// Insert the error icon at the correct position
const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`);
if ($reasoningModelIcon.length > 0) {
$reasoningModelIcon.after($errorIcon);
} else {
$el.prepend($errorIcon);
}
} else if (!hasErrorType && existingErrorIcon.length > 0) {
// Remove the error icon if no longer needed
existingErrorIcon.remove();
} else if (hasErrorType && existingErrorIcon.length > 0) {
// Update the error icon title if it changed
if (existingErrorIcon.attr('data-error-type') !== modelDescription.errorType) {
existingErrorIcon
.attr('data-error-type', modelDescription.errorType)
.attr('title', modelDescription.errorString || 'Error: Used fallback model');
}
}
// Handle model icon
if (config.modelLabelIcons && config.modelLabelIcons !== MODEL_LABEL_ICONS.OFF) {
const existingIcon = $el.find(`.${modelIconCls}`);
// Get model-specific icon based on model name
const modelName = modelDescription?.nameEn ?? '';
const brandIconInfo = getBrandIconInfo(modelName);
if (!brandIconInfo) {
console.warn('[handleModelLabel] brandIconInfo is null', { modelName, modelDescription });
return;
}
const { iconName, brandColor } = brandIconInfo;
const existingIconData = existingIcon.attr('data-model-icon');
const existingIconMode = existingIcon.attr('data-icon-mode');
// Check if we need to update the icon
const shouldUpdateIcon =
existingIconData !== iconName ||
existingIcon.length === 0 ||
existingIconMode !== config.modelLabelIcons;
if (shouldUpdateIcon) {
existingIcon.remove();
if (iconName) {
const iconUrl = getLobeIconsUrl(iconName);
const $icon = jq(`

`)
.attr('data-model-icon', iconName)
.attr('data-icon-mode', config.modelLabelIcons);
// Apply styling based on monochrome/color mode
if (config.modelLabelIcons === MODEL_LABEL_ICONS.MONOCHROME) {
// Apply monochrome filter
$icon.css('filter', 'invert(1)');
// Apply color classes for monochrome icons based on button style
if (config.modelLabelStyle === MODEL_LABEL_STYLE.JUST_TEXT) {
$icon.addClass(iconColorGrayCls);
} else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_CYAN) {
$icon.addClass(iconColorCyanCls);
} else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_SUBTLE) {
$icon.addClass(iconColorGrayCls);
} else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_WHITE) {
$icon.addClass(iconColorWhiteCls);
}
} else if (config.modelLabelIcons === MODEL_LABEL_ICONS.COLOR) {
// Ensure the icon displays in color
$icon.attr('data-brand-color', brandColor);
$icon.css('filter', hexToCssFilter(brandColor).filter);
$icon.attr('data-brand-color-filter', hexToCssFilter(brandColor).filter);
}
const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`);
const $errorIcon = $el.find(`.${errorIconCls}`);
const hasReasoningModelIcon = $reasoningModelIcon.length !== 0;
const hasErrorIcon = $errorIcon.length !== 0;
if (hasReasoningModelIcon) {
// $icon.css({ marginLeft: '0px' });
// $el.css({ paddingRight: hasReasoningModelIcon ? '8px' : '2px' });
$reasoningModelIcon.after($icon);
} else if (hasErrorIcon) {
$errorIcon.after($icon);
} else {
// $icon.css({ marginLeft: '-2px' });
$el.prepend($icon);
}
// if (!modelLabel) {
// $icon.css({ marginRight: '-6px', marginLeft: '-2px' });
// $el.css({ paddingRight: '8px', paddingLeft: '10px' });
// }
}
}
} else {
// Remove model icon if setting is off
$el.find(`.${modelIconCls}`).remove();
}
// Handle reasoning model icon
const isReasoningModel = modelDescription?.modelType === 'reasoning';
if (config.modelLabelUseIconForReasoningModels !== MODEL_LABEL_ICON_REASONING_MODEL.OFF) {
const prevReasoningModelIcon = $el.find(`.${reasoningModelCls}`);
const hasIconSetting = $el.attr('data-reasoning-icon-setting');
const currentSetting = config.modelLabelUseIconForReasoningModels;
const currentIconColor = config.modelLabelReasoningModelIconColor || '#ffffff';
const storedIconColor = $el.attr('data-reasoning-icon-color');
// Only make changes if the reasoning status, icon setting, or color has changed
if (hasIconSetting !== currentSetting ||
(isReasoningModel && prevReasoningModelIcon.length === 0) ||
(!isReasoningModel && prevReasoningModelIcon.length > 0) ||
storedIconColor !== currentIconColor) {
// Update tracking attributes
$el.attr('data-reasoning-icon-setting', currentSetting);
$el.attr('data-reasoning-icon-color', currentIconColor);
$el.attr('data-is-reasoning-model', isReasoningModel);
// Update reasoning model class as needed
if (!isReasoningModel) {
$el.addClass(notReasoningModelCls);
prevReasoningModelIcon.remove();
} else {
$el.removeClass(notReasoningModelCls);
if (prevReasoningModelIcon.length === 0) {
const iconUrl = getLucideIconUrl(config.modelLabelUseIconForReasoningModels.toLowerCase().replace(' ', '-'));
const $icon = jq(`

`);
$icon.css('filter', hexToCssFilter(config.modelLabelReasoningModelIconColor || '#ffffff').filter);
$el.prepend($icon);
const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`);
$reasoningModelIcon.css({ display: 'inline-block' });
if (config.modelLabelStyle === MODEL_LABEL_STYLE.JUST_TEXT) {
$reasoningModelIcon.addClass(iconColorGrayCls);
} else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_CYAN) {
$reasoningModelIcon.addClass(iconColorCyanCls);
} else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_SUBTLE) {
$reasoningModelIcon.addClass(iconColorGrayCls);
} else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_WHITE) {
$reasoningModelIcon.addClass(iconColorWhiteCls);
}
const $modelLabelIcon = $el.find(`.${modelIconCls}`);
const $errorIcon = $el.find(`.${errorIconCls}`);
if ($modelLabelIcon.length !== 0 || $errorIcon.length !== 0) {
$reasoningModelIcon.css({ marginLeft: '4px' });
} else {
$reasoningModelIcon.css({ marginLeft: '0px' });
}
} else {
prevReasoningModelIcon.css('filter', hexToCssFilter(config.modelLabelReasoningModelIconColor || '#ffffff').filter);
}
}
}
}
});
};
const handleHideDiscoverButton = () => {
const config = loadConfigOrDefault();
if (!config.hideDiscoverButton) return;
const $iconsInLeftPanel = PP.getIconsInLeftPanel().find('a[href="/discover"]');
$iconsInLeftPanel.hide();
};
const handleCustomModelPopover = () => {
const config = loadConfigOrDefault();
const mode = config.customModelPopover;
if (mode === CUSTOM_MODEL_POPOVER_MODE.OFF) return;
const $modelSelectionList = PP.getModelSelectionList();
if ($modelSelectionList.length === 0) return;
const processedAttr = 'ph-processed-custom-model-popover';
if ($modelSelectionList.attr(processedAttr)) return;
$modelSelectionList.attr(processedAttr, true);
$modelSelectionList.nthParent(2).css({ maxHeight: 'initial' });
const $reasoningDelim = $modelSelectionList.children(".sm\\:px-sm.relative");
const markListItemAsReasoningModel = (el) => {
const $el = jq(el);
const $icon = jq('
![]()
', {
src: getLucideIconUrl(config.modelLabelUseIconForReasoningModels.toLowerCase()),
alt: 'Reasoning model',
class: reasoningModelCls,
}).css({ marginLeft: '0px' });
$el.find('.cursor-pointer > .flex').first().prepend($icon);
};
const modelSelectionListType = PP.getModelSelectionListType($modelSelectionList);
if (config.modelLabelUseIconForReasoningModels !== MODEL_LABEL_ICON_REASONING_MODEL.OFF) {
if (modelSelectionListType === 'new') {
const $delimIndex = $modelSelectionList.children().index($reasoningDelim);
$modelSelectionList.children().slice($delimIndex + 1).each((_idx, el) => {
markListItemAsReasoningModel(el);
});
} else {
$modelSelectionList
.children()
.filter((_idx, rEl) => jq(rEl).find('span').text().includes('Reasoning'))
.each((_idx, el) => markListItemAsReasoningModel(el));
}
}
const $delims = $modelSelectionList.children(".sm\\:mx-sm");
const removeAllDelims = () => {
$delims.hide();
$reasoningDelim.hide();
};
const removeAllModelDescriptions = () => {
$modelSelectionList.find('div.light.text-textOff').hide();
$modelSelectionList.find('.group\\/item > .relative > .gap-sm').css({ alignItems: 'center' });
};
if (mode === CUSTOM_MODEL_POPOVER_MODE.COMPACT_LIST) {
removeAllDelims();
removeAllModelDescriptions();
return;
}
if (mode === CUSTOM_MODEL_POPOVER_MODE.SIMPLE_LIST) {
// it is already a list, we forced the height to grow
return;
}
$modelSelectionList.css({
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: mode === CUSTOM_MODEL_POPOVER_MODE.COMPACT_GRID ? '0px' : '10px',
'grid-auto-rows': 'min-content',
});
if (mode === CUSTOM_MODEL_POPOVER_MODE.COMPACT_GRID) {
removeAllDelims();
removeAllModelDescriptions();
}
$delims.hide();
$reasoningDelim.css({ gridColumn: 'span 2', });
};
const mainCaptionAppliedCls = genCssName('mainCaptionApplied');
const handleMainCaptionHtml = () => {
const config = loadConfigOrDefault();
if (!config.mainCaptionHtmlEnabled) return;
if (PP.getMainCaption().hasClass(mainCaptionAppliedCls)) return;
PP.setMainCaptionHtml(config.mainCaptionHtml);
PP.getMainCaption().addClass(mainCaptionAppliedCls);
};
const handleCustomJs = () => {
const config = loadConfigOrDefault();
if (!config.customJsEnabled) return;
try {
// Use a static key to ensure we only run once per page load
const dataKey = 'data-' + genCssName('custom-js-applied');
if (!jq('body').attr(dataKey)) {
jq('body').attr(dataKey, true);
// Use Function constructor to evaluate the JS code
const customJsFn = new Function(config.customJs);
customJsFn();
}
} catch (error) {
console.error('Error executing custom JS:', error);
}
};
const handleCustomCss = () => {
const config = loadConfigOrDefault();
if (!config.customCssEnabled) return;
try {
// Check if custom CSS has already been applied
const dataKey = 'data-' + genCssName('custom-css-applied');
if (!jq('head').attr(dataKey)) {
jq('head').attr(dataKey, true);
const styleElement = jq('')
.addClass(customCssAppliedCls)
.text(config.customCss);
jq('head').append(styleElement);
}
} catch (error) {
console.error('Error applying custom CSS:', error);
}
};
const handleCustomWidgetsHtml = () => {
const config = loadConfigOrDefault();
if (!config.customWidgetsHtmlEnabled) return;
try {
// Check if custom widgets have already been applied
const dataKey = 'data-' + genCssName('custom-widgets-html-applied');
if (!jq('body').attr(dataKey)) {
jq('body').attr(dataKey, true);
const widgetContainer = jq('
')
.addClass(customWidgetsHtmlAppliedCls)
.html(config.customWidgetsHtml);
PP.getPromptAreaWrapperOfNewThread().append(widgetContainer);
}
} catch (error) {
console.error('Error applying custom widgets HTML:', error);
}
};
const handleHideSideMenuLabels = () => {
const config = loadConfigOrDefault();
if (!config.hideSideMenuLabels) return;
const $sideMenu = PP.getLeftPanel();
if ($sideMenu.hasClass(sideMenuLabelsHiddenCls)) return;
$sideMenu.addClass(sideMenuLabelsHiddenCls);
};
const handleRemoveWhiteSpaceOnLeftOfThreadContent = () => {
const config = loadConfigOrDefault();
const val = parseFloat(config.leftMarginOfThreadContent);
if (isNaN(val)) return;
if (jq('head').find(`#${leftMarginOfThreadContentStylesId}`).length > 0) return;
jq(``).appendTo("head");
};
// Function to apply a tag's actions (works for both regular and toggle tags)
const applyTagActions = async (tag, options = {}) => {
const { skipText = false, callbacks = {} } = options;
debugLog('Applying tag actions for tag:', tag);
// Apply mode setting
if (tag.setMode) {
const mode = tag.setMode.toLowerCase();
if (mode === 'pro' || mode === 'research' || mode === 'deep-research' || mode === 'dr') {
// Convert aliases to the actual mode name that PP understands
const normalizedMode = mode === 'dr' || mode === 'deep-research' ? 'research' : mode;
try {
await PP.doSelectQueryMode(normalizedMode);
debugLog(`[applyTagActions]: Set mode to ${normalizedMode}`);
wait(50);
} catch (error) {
debugLog(`[applyTagActions]: Error setting mode to ${normalizedMode}`, error);
}
} else {
debugLog(`[applyTagActions]: Invalid mode: ${tag.setMode}`);
}
}
// Apply model setting
if (tag.setModel) {
try {
const modelDescriptor = PP.getModelDescriptorFromId(tag.setModel);
debugLog('[applyTagActions]: set model=', tag.setModel, ' modelDescriptor=', modelDescriptor);
if (modelDescriptor) {
await PP.doSelectModel(modelDescriptor.index);
debugLog(`[applyTagActions]: Selected model ${modelDescriptor.nameEn}`);
if (callbacks.modelSet) callbacks.modelSet(modelDescriptor);
} else {
debugLog(`[applyTagActions]: Model descriptor not found for ${tag.setModel}`);
}
} catch (error) {
debugLog(`[applyTagActions]: Error setting model to ${tag.setModel}`, error);
}
}
// Apply sources setting
if (tag.setSources) {
try {
// Use PP's high-level function that handles the whole process
await PP.doSetSourcesSelectionListValues()(tag.setSources);
debugLog(`[applyTagActions]: Sources set to ${tag.setSources}`);
await PP.sleep(50);
if (callbacks.sourcesSet) callbacks.sourcesSet();
} catch (error) {
logError(`[applyTagActions]: Error setting sources`, error);
}
}
// Add text to prompt if it's not empty and we're not skipping text
if (!skipText && tag.text && tag.text.trim().length > 0) {
try {
const promptArea = PP.getAnyPromptArea();
if (promptArea.length) {
const promptAreaRaw = promptArea[0];
const newText = applyTagToString(tag, promptArea.val(), promptAreaRaw.selectionStart);
changeValueUsingEvent(promptAreaRaw, newText);
debugLog(`[applyTagActions]: Applied text: "${tag.text.substring(0, 20)}${tag.text.length > 20 ? '...' : ''}"`);
if (callbacks.textApplied) callbacks.textApplied(newText);
} else {
debugLog(`[applyTagActions]: No prompt area found for text insertion`);
}
} catch (error) {
debugLog(`[applyTagActions]: Error applying text`, error);
}
}
};
// Function to apply toggled tags' actions when submit is clicked
const applyToggledTagsOnSubmit = async ($wrapper) => {
debugLog('Applying toggled tags on submit', { $wrapper });
const config = loadConfigOrDefault();
const allTags = parseTagsText(config.tagsText ?? defaultConfig.tagsText);
const currentContainerType = getPromptWrapperTagContainerType($wrapper);
if (!currentContainerType) {
logError('Could not determine current container type, skipping toggled tags application', {
$wrapper,
currentContainerType,
allTags,
});
return false;
}
// Find all toggled tags that are relevant for the current container type
const toggledTags = allTags.filter(tag => {
// First check if it's a toggle tag
if (!tag.toggleMode) return false;
const tagId = generateToggleTagId(tag);
if (!tagId) return false;
// Check in-memory toggle state first
const inMemoryToggled = window._phTagToggleState && window._phTagToggleState[tagId] === true;
// Then fall back to saved state if tagToggleSave is enabled
const savedToggled = config.tagToggleSave && config.tagToggledStates && config.tagToggledStates[tagId] === true;
// If neither is toggled, return false
if (!inMemoryToggled && !savedToggled) return false;
// Then check if this tag is relevant for the current container
return isTagRelevantForContainer(currentContainerType)(tag);
});
debugLog(`Toggled tags for ${currentContainerType} context:`, toggledTags.length);
// Apply each toggled tag's actions sequentially, waiting for each to complete
for (const tag of toggledTags) {
debugLog(`Applying toggled tag: ${tag.label || 'Unnamed tag'}`);
try {
await applyTagActions(tag);
debugLog(`Successfully applied toggled tag: ${tag.label || 'Unnamed tag'}`);
} catch (error) {
logError(`Error applying toggled tag: ${tag.label || 'Unnamed tag'}`, error);
}
}
return toggledTags.length > 0;
};
// Function to check if there are active toggled tags
const hasActiveToggledTags = () => {
const config = loadConfigOrDefault();
// Check in-memory toggle states first
if (window._phTagToggleState && Object.values(window._phTagToggleState).some(state => state === true)) {
return true;
}
// Then check saved toggle states if enabled
if (!config.tagToggleSave || !config.tagToggledStates) return false;
// Check if any tags are toggled on in saved state
return Object.values(config.tagToggledStates).some(state => state === true);
};
// Function to check if there are active toggled tags for the current context
const hasActiveToggledTagsForCurrentContext = ($wrapper) => {
const config = loadConfigOrDefault();
const currentContainerType = getPromptWrapperTagContainerType($wrapper);
// START DEBUG LOGGING
const wrapperId = $wrapper && $wrapper.length ? ($wrapper.attr('id') || 'wrapper-' + Math.random().toString(36).substring(2, 9)) : 'no-wrapper';
if (!$wrapper || !$wrapper.length) {
debugLogTags(`hasActiveToggledTagsForCurrentContext - No valid wrapper provided for ${wrapperId}`);
return false;
}
if (!currentContainerType) {
debugLogTags(`hasActiveToggledTagsForCurrentContext - No container type for wrapper ${wrapperId}`);
return false;
}
debugLogTags(`hasActiveToggledTagsForCurrentContext - Container type ${currentContainerType} for wrapper ${wrapperId}`);
// END DEBUG LOGGING
// Get all tags
const allTags = parseTagsText(config.tagsText ?? defaultConfig.tagsText);
// Filter for toggled-on tags relevant to the current context
const hasActiveTags = allTags.some(tag => {
if (!tag.toggleMode) return false;
const tagId = generateToggleTagId(tag);
if (!tagId) return false;
// Check in-memory toggle state first
const inMemoryToggled = window._phTagToggleState && window._phTagToggleState[tagId] === true;
// Then fall back to saved state if tagToggleSave is enabled
const savedToggled = config.tagToggleSave && config.tagToggledStates && config.tagToggledStates[tagId] === true;
// If neither is toggled, return false
if (!inMemoryToggled && !savedToggled) return false;
// Check if this tag is relevant for the current container
const isRelevant = isTagRelevantForContainer(currentContainerType)(tag);
// DEBUG LOG
if (inMemoryToggled || savedToggled) {
debugLogTags(`hasActiveToggledTagsForCurrentContext - Tag ${tag.label || 'unnamed'}: inMemory=${inMemoryToggled}, saved=${savedToggled}, relevant=${isRelevant}`);
}
return isRelevant;
});
// DEBUG LOG
debugLogTags(`hasActiveToggledTagsForCurrentContext - Final result for ${wrapperId}: ${hasActiveTags}`);
return hasActiveTags;
};
// Function to get a comma-separated list of active toggled tag labels
const getActiveToggledTagLabels = ($wrapper) => {
const config = loadConfigOrDefault();
// Get all tags
const allTags = parseTagsText(config.tagsText ?? defaultConfig.tagsText);
// Filter for toggled-on tags
const activeTags = allTags.filter(tag => {
if (!tag.toggleMode) return false;
const tagId = generateToggleTagId(tag);
if (!tagId) return false;
// Check in-memory toggle state first
const inMemoryToggled = window._phTagToggleState && window._phTagToggleState[tagId] === true;
// Then fall back to saved state if tagToggleSave is enabled
const savedToggled = config.tagToggleSave && config.tagToggledStates && config.tagToggledStates[tagId] === true;
// If neither is toggled, return false
if (!inMemoryToggled && !savedToggled) return false;
// If wrapper is provided, check if this tag is relevant for the current container type
if ($wrapper) {
const currentContainerType = getPromptWrapperTagContainerType($wrapper);
if (currentContainerType && !isTagRelevantForContainer(currentContainerType)(tag)) {
return false;
}
}
return true;
});
// Return labels joined by commas
return activeTags.map(tag => tag.label || 'Unnamed tag').join(', ');
};
const mockChromeRuntime = () => {
if (!window.chrome) {
window.chrome = {};
}
if (!window.chrome.runtime) {
window.chrome.runtime = {
_about: 'mock by Perplexity Helper; otherwise clicking on the submit button programmatically crashes in promise',
sendMessage: function() {
log('mockChromeRuntime: sendMessage', arguments);
return Promise.resolve({success: true});
}
};
}
};
// Enhanced submit button for toggled tags
const createEnhancedSubmitButton = (originalButton) => {
const $originalBtn = jq(originalButton);
const config = loadConfigOrDefault();
// Find the proper prompt area wrapper, going up to queryBox class first
const $queryBox = $originalBtn.closest(`.${queryBoxCls}`);
const $wrapper = $queryBox.length
? $queryBox.parent()
: $originalBtn.closest('.flex').parent().parent().parent();
const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper);
const activeTagLabels = getActiveToggledTagLabels($wrapper);
const title = activeTagLabels
? `Submit with toggled tags applied (${activeTagLabels})`
: 'Submit with toggled tags applied';
const $enhancedBtn = jq('
')
.addClass(enhancedSubmitButtonCls)
.attr('title', title)
// ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls
.toggleClass('active', hasActiveInContext && config.tagToggleModeIndicators)
.html(`
PH`);
// Add the enhanced button as an overlay on the original
$originalBtn.css('position', 'relative');
$originalBtn.append($enhancedBtn);
// Handle click on enhanced button
$enhancedBtn.on('click', async (e) => {
e.preventDefault();
e.stopPropagation();
// Show temporary processing indicator
// ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls
$enhancedBtn.addClass('active').css('opacity', '1').find(`.${enhancedSubmitButtonPhTextCls}`).text('...');
// DEBUG
if (loadConfigOrDefault().debugTagsMode) {
debugLogTags(`Enhanced button click - adding 'active' class (should be ${enhancedSubmitButtonActiveCls})`);
}
const finishProcessing = () => {
if (loadConfigOrDefault().debugTagsSuppressSubmit) {
log('Suppressing submit after applying tags');
return;
}
try {
// $originalBtn[0].click();
// const event = new MouseEvent('click', {
// bubbles: true,
// cancelable: true,
// });
// $originalBtn[0].dispatchEvent(event);
// $originalBtn.trigger('click');
// Try to make a more authentic-looking click event
// const clickEvent = new MouseEvent('click', {
// bubbles: true,
// cancelable: true,
// view: window,
// detail: 1, // number of clicks
// isTrusted: true // attempt to make it look trusted (though this is readonly)
// });
// $originalBtn[0].dispatchEvent(clickEvent);
// Find the React component's props
// const reactInstance = Object.keys($originalBtn[0]).find(key => key.startsWith('__reactFiber$'));
// if (reactInstance) {
// const props = $originalBtn[0][reactInstance].memoizedProps;
// if (props && props.onClick) {
// // Call the handler directly, bypassing the event system
// props.onClick();
// } else {
// logError('[createEnhancedSubmitButton]: No onClick handler found', {
// $originalBtn,
// reactInstance,
// props,
// });
// }
// } else {
// logError('[createEnhancedSubmitButton]: No React instance found', {
// $originalBtn,
// });
// }
mockChromeRuntime();
$originalBtn.trigger('click');
} catch (error) {
logError('[createEnhancedSubmitButton]: Error in finishProcessing:', error);
}
};
try {
// Apply all toggled tags sequentially, waiting for each to complete
const tagsApplied = await applyToggledTagsOnSubmit($wrapper);
// Add a small delay after applying all tags to ensure UI updates are complete
if (tagsApplied) { await PP.sleep(50); }
// Reset the button appearance
$enhancedBtn.css('opacity', '').find(`.${enhancedSubmitButtonPhTextCls}`).text('');
if (!hasActiveInContext) {
// ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls
$enhancedBtn.removeClass('active');
// DEBUG
if (loadConfigOrDefault().debugTagsMode) {
debugLogTags(`Enhanced button - removing 'active' class because !hasActiveInContext (should be ${enhancedSubmitButtonActiveCls})`);
}
}
// Trigger the original button click
finishProcessing();
} catch (error) {
console.error('Error in enhanced submit button:', error);
$enhancedBtn.css('opacity', '').find(`.${enhancedSubmitButtonPhTextCls}`).text('');
if (!hasActiveInContext) {
// ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls
$enhancedBtn.removeClass('active');
// DEBUG
if (loadConfigOrDefault().debugTagsMode) {
debugLogTags(`Enhanced button error handler - removing 'active' class (should be ${enhancedSubmitButtonActiveCls})`);
}
}
// Still attempt to submit even if there was an error
finishProcessing();
}
});
return $enhancedBtn;
};
// Add enhanced submit buttons to handle toggled tags
const patchSubmitButtonsForToggledTags = () => {
const config = loadConfigOrDefault();
// Skip if toggle mode hooks are disabled
if (!config.toggleModeHooks) return;
const submitButtons = PP.getSubmitButtonAnyExceptMic();
if (!submitButtons.length) return;
submitButtons.each((_, btn) => {
const $btn = jq(btn);
if ($btn.attr('data-patched-for-toggled-tags')) return;
// Create our enhanced button overlay
createEnhancedSubmitButton(btn);
// Mark as patched
$btn.attr('data-patched-for-toggled-tags', 'true');
});
};
// Function to add keypress listeners to prompt areas
const updateTextareaIndicator = ($textarea) => {
if (!$textarea || !$textarea.length) return;
// Get the current config
const config = loadConfigOrDefault();
// Get the wrapper
const $wrapper = PP.getAnyPromptAreaWrapper($textarea.nthParent(4));
if (!$wrapper || !$wrapper.length) return;
// Check for active toggled tags in this context
const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper);
// Should we show the indicator?
const shouldShowIndicator = hasActiveInContext && config.tagToggleModeIndicators;
// Get current state to avoid unnecessary DOM updates
const currentlyHasClass = $textarea.hasClass(promptAreaKeyListenerCls);
const currentlyHasIndicator = $textarea.siblings(`.${promptAreaKeyListenerIndicatorCls}`).length > 0;
// Only update DOM if state has changed
if (currentlyHasClass !== shouldShowIndicator || currentlyHasIndicator !== shouldShowIndicator) {
if (shouldShowIndicator) {
// Apply the class for the glow effect with transition if not already applied
if (!currentlyHasClass) {
$textarea.addClass(promptAreaKeyListenerCls);
}
// Add the pulse dot indicator if not already present
if (!currentlyHasIndicator) {
// Make sure parent has relative positioning for proper indicator positioning
const $parent = $textarea.parent();
if ($parent.css('position') !== 'relative') {
$parent.css('position', 'relative');
}
const $indicator = jq('
')
.addClass(promptAreaKeyListenerIndicatorCls)
.attr('title', 'Toggle tags active - Press Enter to submit');
$textarea.after($indicator);
// Force a reflow then add visible class for animation
$indicator[0].offsetHeight; // Force reflow
$indicator.addClass('visible');
}
} else {
// Remove the class with transition for fade out
if (currentlyHasClass) {
$textarea.removeClass(promptAreaKeyListenerCls);
}
// For indicator, first make it invisible with transition, then remove from DOM
if (currentlyHasIndicator) {
const $indicator = $textarea.siblings(`.${promptAreaKeyListenerIndicatorCls}`);
$indicator.removeClass('visible');
// Remove from DOM after transition completes
setTimeout(() => {
if ($indicator.length) $indicator.remove();
}, 500); // Match the transition duration in CSS
}
}
}
};
const addPromptAreaKeyListeners = () => {
const config = loadConfigOrDefault();
// Skip if toggle mode hooks are disabled
if (!config.toggleModeHooks) return;
// Get all prompt areas
const promptAreas = PP.getAnyPromptArea();
if (!promptAreas.length) return;
// Process textareas that don't have listeners yet
promptAreas.each((_, textarea) => {
const $textarea = jq(textarea);
// Skip if already has a listener
if ($textarea.attr('data-toggle-keypress-listener')) return;
// Mark as having a listener to avoid duplicates
$textarea.attr('data-toggle-keypress-listener', 'true');
// Add the visual indicator if needed
updateTextareaIndicator($textarea);
// Add the keypress listener for Enter key
$textarea.on('keydown.togglehook', (e) => {
// Only handle Enter key
if (e.key === 'Enter' && !e.shiftKey) {
// Find the wrapper
const $wrapper = PP.getAnyPromptAreaWrapper($textarea.nthParent(4));
if (!$wrapper || !$wrapper.length) return;
// Check if there are active toggled tags for this context
const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper);
if (!hasActiveInContext) return;
// Prevent default behavior
e.preventDefault();
e.stopPropagation();
// Flash the textarea indicator with animation that always plays
$textarea.removeClass(pulseFocusCls);
$textarea[0].offsetHeight; // Force reflow to ensure animation plays
$textarea.addClass(pulseFocusCls);
setTimeout(() => $textarea.removeClass(pulseFocusCls), 400);
// Find and click the submit button
const $submitBtn = PP.submitButtonAny($wrapper);
// If we found a submit button with an enhanced button overlay, use that
if ($submitBtn.length && $submitBtn.find(`.${enhancedSubmitButtonCls}`).length) {
$submitBtn.find(`.${enhancedSubmitButtonCls}`).click();
} else if ($submitBtn.length) {
// Otherwise use the regular submit button
$submitBtn.click();
}
return false;
}
});
// Add focus handling to update appearance
$textarea.on('focus.togglehook', () => {
// Update indicator on focus
updateTextareaIndicator($textarea);
});
});
};
const updateToggleIndicators = () => {
const config = loadConfigOrDefault();
// Track state changes with this object for debugging
const debugStateChanges = {
totalButtons: 0,
unchanged: 0,
titleChanged: 0,
activeStateChanged: 0,
stateChanges: []
};
// Update all enhanced submit buttons individually
jq(`.${enhancedSubmitButtonCls}`).each((idx, btn) => {
const $btn = jq(btn);
const $originalBtn = $btn.parent();
const btnId = $btn.attr('id') || `btn-${idx}`;
debugStateChanges.totalButtons++;
// Find the proper prompt area wrapper, going up to queryBox class first
const $queryBox = $originalBtn.closest(`.${queryBoxCls}`);
const $wrapper = $queryBox.length
? $queryBox.parent()
: $originalBtn.closest('.flex').parent().parent().parent();
// DEBUGGING - Track button's wrapper
const wrapperId = $wrapper && $wrapper.length ? ($wrapper.attr('id') || 'wrapper-' + Math.random().toString(36).substring(2, 9)) : 'no-wrapper';
if (loadConfigOrDefault().debugTagsMode) {
debugLogTags(`updateToggleIndicators - Button ${btnId} in wrapper ${wrapperId}`);
}
const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper);
const activeTagLabels = getActiveToggledTagLabels($wrapper);
const title = activeTagLabels
? `Submit with toggled tags applied (${activeTagLabels})`
: 'Submit with toggled tags applied';
// Get current state to avoid unnecessary DOM updates
// ISSUE: using hard-coded 'active' class instead of generated enhancedSubmitButtonActiveCls
const isCurrentlyActive = $btn.hasClass('active');
const shouldBeActive = hasActiveInContext && config.tagToggleModeIndicators;
// DEBUG - Log the class mismatch
if (loadConfigOrDefault().debugTagsMode) {
const hasGeneratedClass = $btn.hasClass(enhancedSubmitButtonActiveCls);
if (isCurrentlyActive !== hasGeneratedClass) {
debugLogTags(`Class mismatch detected for ${btnId}: 'active'=${isCurrentlyActive}, '${enhancedSubmitButtonActiveCls}'=${hasGeneratedClass}`);
}
}
const currentTitle = $btn.attr('title');
// DEBUGGING - Track state for this button
const stateChange = {
btnId,
wrapperId,
isCurrentlyActive,
shouldBeActive,
stateChanged: isCurrentlyActive !== shouldBeActive,
titleChanged: currentTitle !== title
};
debugStateChanges.stateChanges.push(stateChange);
// Only update DOM elements if state has actually changed
if (isCurrentlyActive !== shouldBeActive || currentTitle !== title) {
// Update title if changed
if (currentTitle !== title) {
debugStateChanges.titleChanged++;
$btn.attr('title', title);
}
// Toggle active class with transition effect if state has changed
if (isCurrentlyActive !== shouldBeActive) {
debugStateChanges.activeStateChanged++;
// ISSUE: We're using literal 'active' here instead of enhancedSubmitButtonActiveCls
// This should be fixed to use the generated class, but we're just logging for now
// No additional class manipulation needed - CSS transitions handle the animation
$btn.toggleClass('active', shouldBeActive);
if (loadConfigOrDefault().debugTagsMode) {
debugLogTags(`Class toggle for ${btnId}: 'active' changed to ${shouldBeActive}, from ${isCurrentlyActive}`);
}
// If transitioning to active, ensure we have proper z-index to show over other elements
if (shouldBeActive) {
$originalBtn.css('z-index', '5');
} else {
// Reset z-index after transition
setTimeout(() => $originalBtn.css('z-index', ''), 500);
}
}
// Update outline only if debugging state requires it
$btn.css({ outline: config.debugTagsSuppressSubmit ? '5px solid red' : 'none' });
} else {
debugStateChanges.unchanged++;
}
});
// Log state change stats
if (loadConfigOrDefault().debugTagsMode) {
if (debugStateChanges.activeStateChanged > 0) {
debugLogTags(`updateToggleIndicators - SUMMARY: total=${debugStateChanges.totalButtons}, unchanged=${debugStateChanges.unchanged}, titleChanged=${debugStateChanges.titleChanged}, activeStateChanged=${debugStateChanges.activeStateChanged}`);
debugLogTags('updateToggleIndicators - State changes:', debugStateChanges.stateChanges.filter(sc => sc.stateChanged));
}
}
// Also update all textarea indicators when toggle mode hooks are enabled
if (config.toggleModeHooks) {
// Get all prompt areas with keypress listeners
const promptAreas = jq('textarea[data-toggle-keypress-listener="true"]');
if (promptAreas.length) {
promptAreas.each((_, textarea) => {
updateTextareaIndicator(jq(textarea));
});
}
}
};
// Function to reset all toggle states (both in-memory and saved if tagToggleSave is enabled)
const resetAllToggleStates = () => {
// Reset in-memory state
window._phTagToggleState = {};
// Reset saved state if tagToggleSave is enabled
const config = loadConfigOrDefault();
if (config.tagToggleSave && config.tagToggledStates) {
const updatedConfig = {
...config,
tagToggledStates: {}
};
saveConfig(updatedConfig);
}
// Update existing toggle tags directly in the DOM if possible
const existingToggledTags = jq(`.${tagCls}[data-toggled="true"]`);
if (existingToggledTags.length > 0) {
existingToggledTags.each((_, el) => {
const $el = jq(el);
const tagData = JSON.parse($el.attr('data-tag') || '{}');
if (tagData) {
// Reset visual state back to untoggled
updateToggleTagState($el, tagData, false);
}
});
} else {
// If we couldn't find any toggled tags in the DOM (perhaps they were added after),
// fall back to a full refresh
refreshTags({ force: true });
}
// Update indicators
updateToggleIndicators();
};
// Function to generate a consistent ID for toggle tags
const generateToggleTagId = tag => {
if (!tag.toggleMode) return null;
return `toggle:${(tag.label || '') + ':' + (tag.position || '') + ':' + (tag.color || '')}:${tag.originalIndex || 0}`;
};
const work = () => {
handleModalCreation();
handleTopSettingsButtonInsertion();
handleTopSettingsButtonSetup();
handleSettingsInit();
handleLeftSettingsButtonSetup();
handleExtraSpaceBellowLastAnswer();
handleHideDiscoverButton();
handleHideSideMenuLabels();
handleRemoveWhiteSpaceOnLeftOfThreadContent();
updateToggleIndicators();
patchSubmitButtonsForToggledTags();
addPromptAreaKeyListeners();
const regex = /^https:\/\/www\.perplexity\.ai\/search\/?.*/;
const currentUrl = jq(location).attr('href');
const matchedCurrentUrlAsSearchPage = regex.test(currentUrl);
// debugLog("currentUrl", currentUrl);
// debugLog("matchedCurrentUrlAsSearchPage", matchedCurrentUrlAsSearchPage);
if (matchedCurrentUrlAsSearchPage) {
handleSearchPage();
}
};
const fastWork = () => {
handleCustomModelPopover();
handleSlimLeftMenu();
handleHideHomeWidgets();
applySideMenuHiding();
replaceIconsInMenu();
handleModelLabel();
handleMainCaptionHtml();
handleCustomJs();
handleCustomCss();
handleCustomWidgetsHtml();
};
const fontUrls = {
Roboto: 'https://fonts.cdnfonts.com/css/roboto',
Montserrat: 'https://fonts.cdnfonts.com/css/montserrat',
Lato: 'https://fonts.cdnfonts.com/css/lato',
Oswald: 'https://fonts.cdnfonts.com/css/oswald-4',
Raleway: 'https://fonts.cdnfonts.com/css/raleway-5',
'Ubuntu Mono': 'https://fonts.cdnfonts.com/css/ubuntu-mono',
Nunito: 'https://fonts.cdnfonts.com/css/nunito',
Poppins: 'https://fonts.cdnfonts.com/css/poppins',
'Playfair Display': 'https://fonts.cdnfonts.com/css/playfair-display',
Merriweather: 'https://fonts.cdnfonts.com/css/merriweather',
'Fira Sans': 'https://fonts.cdnfonts.com/css/fira-sans',
Quicksand: 'https://fonts.cdnfonts.com/css/quicksand',
Comfortaa: 'https://fonts.cdnfonts.com/css/comfortaa-3',
'Almendra': 'https://fonts.cdnfonts.com/css/almendra',
'Enchanted Land': 'https://fonts.cdnfonts.com/css/enchanted-land',
'Cinzel Decorative': 'https://fonts.cdnfonts.com/css/cinzel-decorative',
'Orbitron': 'https://fonts.cdnfonts.com/css/orbitron',
'Exo 2': 'https://fonts.cdnfonts.com/css/exo-2',
'Chakra Petch': 'https://fonts.cdnfonts.com/css/chakra-petch',
'Open Sans Condensed': 'https://fonts.cdnfonts.com/css/open-sans-condensed',
'Saira Condensed': 'https://fonts.cdnfonts.com/css/saira-condensed',
Inter: 'https://cdn.jsdelivr.net/npm/@fontsource/inter@4.5.0/index.min.css',
'JetBrains Mono': 'https://fonts.cdnfonts.com/css/jetbrains-mono',
};
const loadFont = (fontName) => {
const fontUrl = fontUrls[fontName];
debugLog('loadFont', { fontName, fontUrl });
if (fontUrl) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = fontUrl;
document.head.appendChild(link);
}
};
const setupFixImageGenerationOverlay = () => {
const config = loadConfigOrDefault();
if (config.fixImageGenerationOverlay) {
setInterval(handleFixImageGenerationOverlay, 250);
}
};
(function () {
if (loadConfigOrDefault()?.debugMode) {
enableDebugMode();
}
debugLog('TAGS_PALETTES', TAGS_PALETTES);
if (loadConfigOrDefault()?.debugTagsMode) {
enableTagsDebugging();
}
// Initialize in-memory toggle state from saved state if tagToggleSave is enabled
const config = loadConfigOrDefault();
if (config.tagToggleSave && config.tagToggledStates) {
window._phTagToggleState = { ...config.tagToggledStates };
debugLog('Initialized in-memory toggle state from saved state', window._phTagToggleState);
} else {
window._phTagToggleState = {};
}
'use strict';
jq("head").append(``);
setupTags();
setupFixImageGenerationOverlay();
const mainInterval = setInterval(work, 1000);
// This interval is too fast (100ms) which causes frequent DOM updates
// and leads to the class toggling issue with 'active' vs enhancedSubmitButtonActiveCls
const fastInterval = setInterval(fastWork, 100);
window.ph = {
stopWork: () => { clearInterval(mainInterval); clearInterval(fastInterval); },
work,
fastWork,
jq,
showPerplexityHelperSettingsModal,
enableTagsDebugging: () => { debugTags = true; },
disableTagsDebugging: () => { debugTags = false; },
};
loadFont(loadConfigOrDefault().tagFont);
loadFont('JetBrains Mono');
// Auto open settings if enabled
if (loadConfigOrDefault()?.autoOpenSettings) {
// Use setTimeout to ensure the DOM is ready
setTimeout(() => {
showPerplexityHelperSettingsModal();
}, 1000);
}
console.log(`%c${userscriptName}%c\n %cTiartyos%c & %cmonnef%c\n ... loaded`,
'color: #aaffaa; font-size: 1.5rem; background-color: rgba(0, 0, 0, 0.5); padding: 2px;',
'',
'color: #6b02ff; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;',
'',
'color: #aa2cc3; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;',
'',
'');
console.log('to show settings use:\nph.showPerplexityHelperSettingsModal()');
}());