From b0ac73dc3f9fd2c7610dd942c0d91c337f2704c3 Mon Sep 17 00:00:00 2001 From: Saeed Ezzati Date: Sun, 11 Jun 2023 14:31:06 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20lots=20of=20new=20featur?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Custom instruction An instruction that will be added to the end of each prompt. For example, you can add "Please repeat the prompt after me.", or "Please refrain from writing warnings about your knowledge cutoff" to the custom instruction, and it will be added to the end of every prompt. - You can now export/import prompt history. Simply go to My Prompt History, and you'll see two new buttons at the bottom of the menu. - You can now also import/export your Settings, Custom Prompts (Shortcuts), and Folder Structure. You can find this feature in the Settings menu under the General tab. - Auto Click. You can now enable auto-click on your custom prompt. By enabling this button, your "default" custom prompt will be clicked automatically at the end of each response. - Smart Replace. Custom prompts are now even more powerful. Your custom prompt now also acts as a shortcut. To use shortcuts in your prompt, simply type @ followed by the name of the prompt and press space. Your text will automatically be replaced by the custom prompt text. - Prompt Templates. We now support prompt templates using curly brackets. If you try to submit a prompt that includes a word surrounded by {{two curly brackets}}, a new popup shows up and asks you to enter your desired values for those curly brackets. Make sure to use "{{ }}" when submitting new prompts to the community. - Added a popup menu to the extension icon for easy access to various links - Improved Auto-Sync - Fixed the style issue with the Continue button not being correctly placed - Fixed the list styling issue --- README.md | 16 +- manifest.json | 4 +- popup.css | 19 ++ popup.html | 34 +++ scripts/content/announcement.js | 14 +- scripts/content/api.js | 27 ++- scripts/content/autoSave.js | 9 +- scripts/content/continue.js | 75 ++++-- scripts/content/conversation.js | 17 +- scripts/content/conversationList.js | 325 +++++++++++++++----------- scripts/content/global.js | 3 +- scripts/content/pluginStore.js | 4 +- scripts/content/promptHistory.js | 129 +++++++++- scripts/content/regenerateResponse.js | 2 +- scripts/content/settings.js | 137 ++++++++++- scripts/content/shareModal.js | 4 +- scripts/content/templateWordsModal.js | 60 +++++ scripts/styles/global.css | 3 + 18 files changed, 664 insertions(+), 218 deletions(-) create mode 100644 popup.css create mode 100644 popup.html create mode 100644 scripts/content/templateWordsModal.js diff --git a/README.md b/README.md index a16dbb4..408d6fb 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A browser extension to add the missing features like **Folders**, **Search**, an [![Mozilla Add-on](https://img.shields.io/amo/users/superpower-chatgpt.svg)]([link-firefox]) [![Mozilla Add-on](https://img.shields.io/amo/rating/superpower-chatgpt.svg)]([link-firefox]) -[![Discord](https://img.shields.io/discord/1060110102188797992?color=green&label=Discord&logo=discord)](https://discord.gg/Ma9VpTSct2) +[![Discord](https://img.shields.io/discord/1060110102188797992?color=green&label=Discord&logo=discord)](https://discord.gg/superpower-chatgpt-1083455984489476220) [![Twitter Follow](https://img.shields.io/twitter/follow/eeeziii?label=follow%20me&style=social)](https://twitter.com/intent/user?screen_name=eeeziii)
@@ -35,7 +35,7 @@ https://user-images.githubusercontent.com/574142/232172841-50f1b114-ef47-4533-a6 🔁 Auto Sync: Never lose your chats. Automatically sync a copy of all your chats on ChatGPT to your computer -📥 Export: Select and Export any number of your chats into multiple formats(.txt, .json, .md) +📥 Export: Select and Export any number of your chats into multiple formats(.txt, .json, .md). You can also export your prompt history, your settings, your shortcuts and your folders. 🔎 Search and Highlight: Search through all your previous chats on ChatGPT and highlight results for quick review @@ -59,6 +59,8 @@ https://user-images.githubusercontent.com/574142/232172841-50f1b114-ef47-4533-a6 ⭐ Favorite prompts: Mark your prompts as favorite in your prompt history +📄 Prompt templates: put words inside {{double curly brackets}} in your prompt, and you'll be asked to replace them before submitting the prompt + 🔍 Search Function: Easily search through your prompt history and hundreds of prompt examples from the ChatGPT community 📜 Community Prompts: Get inspiration from hundreds of other prompts created by the ChatGPT Community and share your prompts too. Upvote, downvote, and report prompts, and sort them by the most used or most upvoted. Filter prompts by category and language @@ -81,6 +83,12 @@ https://user-images.githubusercontent.com/574142/232172841-50f1b114-ef47-4533-a6 📏 Custom Conversation Width: Adjust the width of the conversation to your liking +🔄 Smart Replace: Automatically replace pre-defined phrases with longer text as you type prompts + +🖱️ Auto Click: Automatically click on the defaul custom prompt button at the end of each response + +👉 Custom Instruction: Don't epeat yourself. Automatically add a custom instruction to the end of each prompt + 📊 Word and Character Count: Add the word and character counters to both the user input and the ChatGPT responses 🎛 Model Switcher: Easily change the model(GPT-4, GPT-3.5, etc.) in the middle of the conversation. Simply hover over the ChatGPT avatar icon to see what model was used for each response @@ -175,7 +183,7 @@ Read our [FAQ document](https://ezi.notion.site/Superpower-ChatGPT-FAQ-9d43a8a1c ## Feature Requests, Bugs, and Support -Join our [Discord channel](https://discord.gg/Ma9VpTSct2) to stay up to date, submit feature requests, report bugs, and get faster support +Join our [Discord channel](https://discord.gg/superpower-chatgpt-1083455984489476220) to stay up to date, submit feature requests, report bugs, and get faster support ## Privacy @@ -189,7 +197,7 @@ I appreciate your interest in supporting this extension. Here are some ways you [![Donate with Stripe](https://img.shields.io/badge/stripe-donate-blue.svg)](https://buy.stripe.com/6oE6s0dQS7y2bjG9AA) [![visitors](https://visitor-badge.glitch.me/badge?page_id=saeedezzati/superpower-chatgpt)](https://visitor-badge.glitch.me) -[![Discord](https://img.shields.io/discord/1060110102188797992?color=green&label=Discord&logo=discord)](https://discord.gg/Ma9VpTSct2) +[![Discord](https://img.shields.io/discord/1060110102188797992?color=green&label=Discord&logo=discord)](https://discord.gg/superpower-chatgpt-1083455984489476220) [![Twitter Follow](https://img.shields.io/twitter/follow/eeeziii?label=follow%20me&style=social)](https://twitter.com/intent/user?screen_name=eeeziii) ## Copyright diff --git a/manifest.json b/manifest.json index 6a2a89c..f013c30 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Superpower ChatGPT", - "version": "3.4.0", + "version": "3.5.0", "description": "ChatGPT with superpowers! Sync/search history locally, create folders, export all chats, pin messages, access thousands of prompts", "icons": { "16": "images/icon-16.png", @@ -10,6 +10,7 @@ "128": "images/icon-128.png" }, "action": { + "default_popup": "popup.html", "16": "images/icon-16.png", "32": "images/icon-32.png", "48": "images/icon-48.png", @@ -53,6 +54,7 @@ "scripts/content/enableMFA.js", "scripts/content/pluginsDropdown.js", "scripts/content/dropdown.js", + "scripts/content/templateWordsModal.js", "scripts/content/shareModal.js", "scripts/content/feedbackModal.js", "scripts/content/toneList.js", diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..907a18f --- /dev/null +++ b/popup.css @@ -0,0 +1,19 @@ +body { + margin: 0; + padding: 4px 8px; + width: 140px; + background-color: gold; +} +button { + height: 36px; + width: 100%; + margin: 4px 0; + border: solid 1px #f1f1f1; + border-radius: 4px; + cursor: pointer; + background-color: white; + &:hover { + background-color: #f1f1f1; + } +} + diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..4ae0ea5 --- /dev/null +++ b/popup.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scripts/content/announcement.js b/scripts/content/announcement.js index 7217f33..983e9e7 100644 --- a/scripts/content/announcement.js +++ b/scripts/content/announcement.js @@ -7,8 +7,8 @@ const subtitleMap = { general: 'You can see the latest announcement here', newsletter: 'Daily dose of AI news and resources from the community', }; -function createAnnouncementModal(data) { - const bodyContent = announcementModalContent(data); +function createAnnouncementModal(data, email = '') { + const bodyContent = announcementModalContent(data, email); const actionsBarContent = announcementModalActions(data); const title = titleMap[data.category]; const subtitle = subtitleMap[data.category]; @@ -45,7 +45,7 @@ function addSponsorElement(sponsor) { sponsor.appendChild(sponsorLink); }); } -function announcementModalContent(data) { +function announcementModalContent(data, email = '') { // create announcement modal content const content = document.createElement('div'); content.id = `modal-content-${data.category}`; @@ -64,7 +64,7 @@ function announcementModalContent(data) { announcementText.style = 'display: flex; flex-direction: column; justify-content: start; align-items: start; min-height: 100%; width: 100%; white-space: break-spaces; overflow-wrap: break-word;padding:16px;position: relative;z-index:10;color: #fff;'; const announcement = data; // add ?ref=superpower-chatgpt-chrome-extension to the end of all href links - const updatedTextWithRef = announcement.text.replace(/href="([^"]*)"/g, 'href="$1?ref=superpower-chatgpt-extension"'); + const updatedTextWithRef = announcement.text.replace(/href="([^"]*)"/g, 'href="$1?ref=superpower-chatgpt-extension"').replace(/\{\{email\}\}/g, email); announcementText.innerHTML = announcement.category === 'newsletter' ? updatedTextWithRef : `

${announcement.title}

${announcement.text}`; // if release_data is before march 21, 2023, add sponsor if (announcement.category === 'newsletter' && new Date(announcement.release_date) < new Date('2023-03-20')) { @@ -120,9 +120,9 @@ function announcementModalActions(data) { // eslint-disable-next-line no-unused-vars function initializeAnnouncement() { setTimeout(() => { - chrome.storage.sync.get(['lastSeenAnnouncementId', 'lastSeenNewsletterId'], (result) => { + chrome.storage.sync.get(['lastSeenAnnouncementId', 'lastSeenNewsletterId', 'email'], (result) => { chrome.storage.local.get(['settings'], (res) => { - const { lastSeenAnnouncementId, lastSeenNewsletterId } = result; + const { lastSeenAnnouncementId, lastSeenNewsletterId, email } = result; // try getting latest announcement first getLatestAnnouncement().then((announcement) => { if (announcement && announcement.id && lastSeenAnnouncementId !== announcement.id) { @@ -134,7 +134,7 @@ function initializeAnnouncement() { getLatestNewsletter().then((newsletter) => { if (!newsletter || !newsletter.id) return; if (lastSeenNewsletterId !== newsletter.id) { - createAnnouncementModal(newsletter); + createAnnouncementModal(newsletter, email); chrome.storage.sync.set({ lastSeenNewsletterId: newsletter.id }); chrome.storage.local.get(['readNewsletterIds'], (results) => { const readNewsletterIds = results.readNewsletterIds || []; diff --git a/scripts/content/api.js b/scripts/content/api.js index 9b6b05b..86c4e2d 100644 --- a/scripts/content/api.js +++ b/scripts/content/api.js @@ -12,11 +12,18 @@ chrome.storage.local.get(['environment'], (result) => { const defaultHeaders = { 'content-type': 'application/json', }; -function generateChat(message, conversationId, messageId, parentMessageId, saveHistory = true, role = 'user', pluginIds = []) { +function generateChat(message, conversationId, messageId, parentMessageId, saveHistory = true, role = 'user', action = 'next') { return chrome.storage.local.get(['settings', 'enabledPluginIds']).then((res) => chrome.storage.sync.get(['auth_token']).then((result) => { const payload = { - action: 'next', - messages: messageId + action, + arkose_token: null, + model: res.settings.selectedModel.slug, + parent_message_id: parentMessageId, + history_and_training_disabled: !saveHistory, + timezone_offset_min: new Date().getTimezoneOffset(), + }; + if (action === 'next') { + payload.messages = messageId ? [ { id: messageId, @@ -27,12 +34,8 @@ function generateChat(message, conversationId, messageId, parentMessageId, saveH }, }, ] - : null, - model: res.settings.selectedModel.slug, - parent_message_id: parentMessageId, - history_and_training_disabled: !saveHistory, - timezone_offset_min: new Date().getTimezoneOffset(), - }; + : null; + } if (conversationId) { payload.conversation_id = conversationId; } @@ -388,7 +391,7 @@ function getAllConversations(forceRefresh = false) { const { limit, offset, items, } = convs; - const total = Math.min(convs.total, 10000); // sync last 10000 conversations + const total = convs.total ? Math.min(convs.total, 10000) : 10000; // sync last 10000 conversations if (items.length === 0 || total === 0) { resolve([]); return; @@ -446,11 +449,11 @@ function getSharedConversations(offset = 0, limit = 100) { return Promise.reject(res); })); } -function getConversations(offset = 0, limit = 100) { +function getConversations(offset = 0, limit = 100, order = 'updated') { const url = new URL('https://chat.openai.com/backend-api/conversations'); // without passing limit it returns 50 by default // limit cannot be more than 20 - const params = { offset, limit }; + const params = { offset, limit, order }; url.search = new URLSearchParams(params).toString(); return chrome.storage.sync.get(['auth_token']).then((result) => fetch(url, { method: 'GET', diff --git a/scripts/content/autoSave.js b/scripts/content/autoSave.js index d2d40e4..af43420 100644 --- a/scripts/content/autoSave.js +++ b/scripts/content/autoSave.js @@ -109,6 +109,7 @@ function updateOrCreateConversation(conversationId, message, parentId, settings, existingConversation.writingStyleCode = settings.selectedWritingStyle.code; existingConversation.shouldRefresh = forceRefresh; existingConversation.current_node = message.id; + existingConversation.update_time = 'force_copy'; if (existingConversation.mapping[message.id]?.id) { existingConversation.mapping[message.id].message = message; } else { @@ -172,7 +173,7 @@ function updateOrCreateConversation(conversationId, message, parentId, settings, current_node: message.id, title: 'New chat', create_time: (new Date()).getTime() / 1000, - update_time: 'initialize', + update_time: 'force_copy', mapping: { [parentId]: { children: [systemMessage.id], id: parentId, message: null, parent: null, @@ -323,7 +324,7 @@ function initializeAutoSave(skipInputFormReload = false, forceRefreshIds = []) { const remoteConv = remoteConversations.find((conv) => conv.id === localConvIds[i]) || localConversations[localConvIds[i]]; localConversations[localConvIds[i]].title = remoteConv.title; // eslint-disable-next-line prefer-destructuring - if (localConversations[localConvIds[i]].update_time === 'initialize') { + if (localConversations[localConvIds[i]].update_time === 'force_copy') { localConversations[localConvIds[i]].update_time = new Date(remoteConv.update_time).getTime() / 1000; } // delete conversation key(legacy) @@ -591,7 +592,7 @@ function initializeAutoSave(skipInputFormReload = false, forceRefreshIds = []) { // if (isGenerating || !canSubmit) return; // const syncDiv = document.getElementById('sync-div'); // syncDiv.remove(); - // const { pathname } = new URL(window.location.toString());; + // const { pathname } = new URL(window.location.toString()); // const conversationId = pathname.split('/').pop().replace(/[^a-z0-9-]/gi, ''); // const refreshIds = []; // if (/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(conversationId)) { @@ -605,7 +606,7 @@ function initializeAutoSave(skipInputFormReload = false, forceRefreshIds = []) { // if (isGenerating || !canSubmit) return; // const syncDiv = document.getElementById('sync-div'); // syncDiv.remove(); - // const { pathname } = new URL(window.location.toString());; + // const { pathname } = new URL(window.location.toString()) // const conversationId = pathname.split('/').pop().replace(/[^a-z0-9-]/gi, ''); // const refreshIds = []; // if (/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(conversationId)) { diff --git a/scripts/content/continue.js b/scripts/content/continue.js index 3bdf742..27b99f1 100644 --- a/scripts/content/continue.js +++ b/scripts/content/continue.js @@ -120,7 +120,7 @@ function addContinueButton() { continueButton.id = 'continue-conversation-button'; continueButton.type = 'button'; continueButton.dir = 'auto'; - continueButton.style = 'width:96px;border-top-left-radius:0;border-bottom-left-radius:0;border-left:0;z-index:1;text-transform: capitalize;'; + continueButton.style = 'width:96px;border-radius:0;border-left:0;z-index:1;text-transform: capitalize;'; continueButton.classList.add('btn', 'block', 'justify-center', 'gap-2', 'btn-neutral', 'border-0', 'md:border', 'max-w-10', 'truncate'); continueButton.addEventListener('click', (e) => { @@ -144,31 +144,56 @@ function addContinueButton() { continueButton.addEventListener('mouseout', () => { shiftClickText.style = 'font-size:10px;position:absolute;left:0px;bottom:40px;display:none;color:lightslategray;width:200px;'; }); - continueButtonWrapper.appendChild(shiftClickText); - continueButtonWrapper.appendChild(continueButtonDropdown); - continueButtonWrapper.appendChild(promptDropdown()); - continueButtonWrapper.appendChild(continueButton); - if (canSubmit) { - const textAreaElement = inputForm.querySelector('textarea'); - if (!textAreaElement) return; - const textAreaElementWrapper = textAreaElement.parentNode; - let nodeBeforetTextAreaElement = textAreaElementWrapper.previousSibling; - if (!nodeBeforetTextAreaElement) { - // create a new div - const newDiv = document.createElement('div'); - newDiv.classList = 'h-full flex ml-1 md:w-full md:m-auto md:mb-2 gap-0 md:gap-2 justify-center'; - // prepent inputform with new div - inputForm.firstChild.prepend(newDiv); - nodeBeforetTextAreaElement = newDiv; - } - if (nodeBeforetTextAreaElement.classList.length === 0) { - nodeBeforetTextAreaElement.classList = 'h-full flex ml-1 md:w-full md:m-auto md:mb-2 gap-0 md:gap-2 justify-center'; - nodeBeforetTextAreaElement.firstChild.classList = ''; - } - nodeBeforetTextAreaElement.style.minHeight = '38px'; - nodeBeforetTextAreaElement.appendChild(continueButtonWrapper); - } + const autoClickButton = document.createElement('button'); + chrome.storage.local.get('settings', ({ settings }) => { + autoClickButton.innerHTML = ``; + }); + autoClickButton.id = 'auto-click-button'; + autoClickButton.type = 'button'; + autoClickButton.title = 'Auto Click'; + autoClickButton.style = 'width:38px;border-top-left-radius:0;border-bottom-left-radius:0;border-left:0;z-index:1;padding:0;'; + autoClickButton.classList.add('btn', 'flex', 'justify-center', 'gap-2', 'btn-neutral', 'border-0', 'md:border'); + autoClickButton.addEventListener('click', () => { + chrome.storage.local.get('settings', ({ settings }) => { + chrome.storage.local.set({ settings: { ...settings, autoClick: !settings.autoClick } }, () => { + autoClickButton.querySelector('svg').setAttribute('stroke', settings.autoClick ? 'currentColor' : '#19c37d'); + autoClickButton.querySelector('svg').setAttribute('fill', settings.autoClick ? 'currentColor' : '#19c37d'); + }); + }); + }); + + chrome.storage.local.get('settings', ({ settings }) => { + setTimeout(() => { + continueButtonWrapper.appendChild(shiftClickText); + continueButtonWrapper.appendChild(continueButtonDropdown); + continueButtonWrapper.appendChild(promptDropdown()); + continueButtonWrapper.appendChild(continueButton); + if (settings.autoSync) { + continueButtonWrapper.appendChild(autoClickButton); + } + if (canSubmit) { + const textAreaElement = inputForm.querySelector('textarea'); + if (!textAreaElement) return; + const textAreaElementWrapper = textAreaElement.parentNode; + let nodeBeforetTextAreaElement = textAreaElementWrapper.previousSibling; + if (!nodeBeforetTextAreaElement) { + // create a new div + const newDiv = document.createElement('div'); + newDiv.classList = 'h-full flex ml-1 md:w-full md:m-auto md:mb-2 gap-0 md:gap-2 justify-center'; + // prepent inputform with new div + inputForm.firstChild.prepend(newDiv); + nodeBeforetTextAreaElement = newDiv; + } + if (nodeBeforetTextAreaElement.classList.length === 0) { + nodeBeforetTextAreaElement.classList = 'h-full flex ml-1 md:w-full md:m-auto md:mb-2 gap-0 md:gap-2 justify-center'; + nodeBeforetTextAreaElement.firstChild.classList = ''; + } + nodeBeforetTextAreaElement.style.minHeight = '38px'; + nodeBeforetTextAreaElement.appendChild(continueButtonWrapper); + } + }, 200); + }); } // eslint-disable-next-line no-unused-vars diff --git a/scripts/content/conversation.js b/scripts/content/conversation.js index 6f436ac..f562a59 100644 --- a/scripts/content/conversation.js +++ b/scripts/content/conversation.js @@ -86,6 +86,13 @@ function loadConversationFromNode(conversationId, newMessageId, oldMessageId, se messageDiv += rowUser(fullConversation, sortedNodes[i], threadIndex, threadCount, result.name, result.avatar, settings.customConversationWidth, settings.conversationWidth, searchValue); } if (message.role === 'assistant' || message.author?.role === 'assistant') { + let nextMessage = sortedNodes[i + 1]?.message; + while (nextMessage && nextMessage.recipient === 'all' && (nextMessage.role === 'assistant' || nextMessage.author?.role === 'assistant')) { + message.content.parts = message.content.parts.concat(nextMessage.content.parts); + i += 1; + nextMessage = sortedNodes[i + 1]?.message; + } + sortedNodes[i].message = message; messageDiv += rowAssistant(fullConversation, sortedNodes[i], threadIndex, threadCount, res.models, settings.customConversationWidth, settings.conversationWidth, searchValue); } } @@ -177,6 +184,13 @@ function loadConversation(conversationId, searchValue = '', focusOnInput = true) messageDiv += rowUser(fullConversation, sortedNodes[i], threadIndex, threadCount, result.name, result.avatar, settings.customConversationWidth, settings.conversationWidth, searchValue); } if (message.recipient === 'all' && (message.role === 'assistant' || message.author?.role === 'assistant')) { + let nextMessage = sortedNodes[i + 1]?.message; + while (nextMessage && nextMessage.recipient === 'all' && (nextMessage.role === 'assistant' || nextMessage.author?.role === 'assistant')) { + message.content.parts = message.content.parts.concat(nextMessage.content.parts); + i += 1; + nextMessage = sortedNodes[i + 1]?.message; + } + sortedNodes[i].message = message; messageDiv += rowAssistant(fullConversation, sortedNodes[i], threadIndex, threadCount, res.models, settings.customConversationWidth, settings.conversationWidth, searchValue); } } @@ -238,7 +252,8 @@ function updateTotalCounter() { let totalCharacters = 0; allMessages.forEach((message) => { const text = message.innerText; - const words = text.split(' '); + const words = text.split(/[\s\n]+/); + totalWords += words.length; totalCharacters += text.length; }); diff --git a/scripts/content/conversationList.js b/scripts/content/conversationList.js index b53739f..9b30684 100644 --- a/scripts/content/conversationList.js +++ b/scripts/content/conversationList.js @@ -1,6 +1,6 @@ /* eslint-disable no-restricted-globals */ // eslint-disable-next-line no-unused-vars -/* global markdown, markdownitSup, initializeNavbar, generateInstructions, generateChat, SSE, formatDate, loadConversation, resetSelection, katex, texmath, rowUser, rowAssistant, updateOrCreateConversation, replaceTextAreaElemet, highlight, isGenerating:true, disableTextInput:true, generateTitle, debounce, initializeRegenerateResponseButton, initializeStopGeneratingResponseButton, toggleTextAreaElemet, showNewChatPage, chatStreamIsClosed:true, addCopyCodeButtonsEventListeners, addScrollDetector, scrolUpDetected:true, Sortable, updateInputCounter, addUserPromptToHistory, getGPT4CounterMessageCapWindow, createFolder, getConversationElementClassList, notSelectedClassList, selectedClassList, conversationActions, addCheckboxToConversationElement, createConversation, deleteConversation, handleQueryParams, addScrollButtons, updateTotalCounter, isWindows, loadSharedConversation */ +/* global markdown, markdownitSup, initializeNavbar, generateInstructions, generateChat, SSE, formatDate, loadConversation, resetSelection, katex, texmath, rowUser, rowAssistant, updateOrCreateConversation, replaceTextAreaElemet, highlight, isGenerating:true, disableTextInput:true, generateTitle, debounce, initializeRegenerateResponseButton, initializeStopGeneratingResponseButton, toggleTextAreaElemet, showNewChatPage, chatStreamIsClosed:true, addCopyCodeButtonsEventListeners, addScrollDetector, scrolUpDetected:true, Sortable, updateInputCounter, addUserPromptToHistory, getGPT4CounterMessageCapWindow, createFolder, getConversationElementClassList, notSelectedClassList, selectedClassList, conversationActions, addCheckboxToConversationElement, createConversation, deleteConversation, handleQueryParams, addScrollButtons, updateTotalCounter, isWindows, loadSharedConversation, createTemplateWordsModal */ // Initial state let userChatIsActuallySaved = false; @@ -403,20 +403,21 @@ function updateNewChatButtonSynced() { resetSelection(); } else { showNewChatPage(); - } - if (textAreaElement) { - textAreaElement.focus(); - } - // remove selected class from conversations from conversations list - const focusedConversations = document.querySelectorAll('.selected'); - focusedConversations.forEach((c) => { - c.classList = notSelectedClassList; - }); - // if search box has value reload conversations list - const searchBox = document.querySelector('#conversation-search'); - if (searchBox?.value) { - searchBox.value = ''; - searchBox.dispatchEvent(new Event('input'), { bubbles: true }); + + if (textAreaElement) { + textAreaElement.focus(); + } + // remove selected class from conversations from conversations list + const focusedConversations = document.querySelectorAll('.selected'); + focusedConversations.forEach((c) => { + c.classList = notSelectedClassList; + }); + // if search box has value reload conversations list + const searchBox = document.querySelector('#conversation-search'); + if (searchBox?.value) { + searchBox.value = ''; + searchBox.dispatchEvent(new Event('input'), { bubbles: true }); + } } }); if (selectedConversations?.length > 0) { @@ -472,9 +473,11 @@ function submitChat(userInput, conversation, messageId, parentId, settings, mode if (userChatIsActuallySaved) { clearInterval(tempId); updateOrCreateConversation(finalConversationId, finalMessage, messageId, settings, true, chatStreamIsClosed).then(() => { - setTimeout(() => { - insertNextChunk(settings, finalMessage); - }, 700); + if (!chatStreamIsClosed) { + setTimeout(() => { + insertNextChunk(settings, finalMessage); + }, 700); + } }); } }, 1000); @@ -721,10 +724,14 @@ function insertNextChunk(settings, previousMessage) { return; } } - if (!settings.autoSplit) return; - if (totalChunks === 1) return; - if (remainingText === '') return; - + if (!settings.autoSplit || totalChunks === 1 || remainingText === '') { + if (settings.autoClick) { + const continueButton = document.getElementById('continue-conversation-button'); + if (!continueButton) return; + continueButton.click(); + } + return; + } const inputForm = document.querySelector('form'); if (!inputForm) return; const submitButton = inputForm.querySelector('textarea ~ button'); @@ -742,6 +749,7 @@ ${settings.autoSplitChunkPrompt}`; textAreaElement.dispatchEvent(new Event('change', { bubbles: true })); submitButton.click(); } + function getLastIndexOf(text, position) { // if text down't include \n or . or ? or ! return position if (!text.includes('\n') && !text.includes('.') && !text.includes('?') && !text.includes('!')) return position; @@ -768,135 +776,155 @@ function overrideSubmitForm() { e.preventDefault(); e.stopPropagation(); if (isGenerating) return; - const { pathname } = new URL(window.location.toString()); - // const isSharedConversation = pathname.startsWith('/share/') && window.location.href.endsWith('/continue'); - const conversationId = pathname.split('/').pop().replace(/[^a-z0-9-]/gi, ''); - const anyUserMessageWrappers = document.querySelectorAll('[id^="message-wrapper-"][data-role="user"]').length > 0; - if (/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(conversationId) && anyUserMessageWrappers) { - chrome.storage.local.get(['conversations', 'settings', 'models']).then((res) => { - const { conversations, settings, models } = res; - const conversation = conversations[conversationId]; - chrome.storage.sync.get(['name', 'avatar'], (result) => { - let text = textAreaElement.value.trim(); - if (chunkNumber === 1) { - finalSummary = ''; - if (settings.autoSplit && text.length > settings.autoSplitLimit) { - totalChunks = Math.ceil(text.length / settings.autoSplitLimit); - const lastNewLineIndexBeforeLimit = settings.autoSplitLimit > text.length ? settings.autoSplitLimit : getLastIndexOf(text, settings.autoSplitLimit); - remainingText = text.substring(lastNewLineIndexBeforeLimit); - text = `${settings.autoSplitInitialPrompt}[START CHUNK ${chunkNumber}/${totalChunks}] + // get all words wrapped in {{ and }} + const templateWords = textAreaElement.value.match(/{{(.*?)}}/g); + if (templateWords?.length > 0) { + // open template words modal and wait for user to select a word. the when user submit, submit the input form with the replacement + createTemplateWordsModal(templateWords); + const firstTemplateWordInput = document.querySelector('[id^=template-input-]'); + if (firstTemplateWordInput) { + firstTemplateWordInput.focus(); + setTimeout(() => { + firstTemplateWordInput.value = ''; + }, 100); + } + } else { + const { pathname } = new URL(window.location.toString()); + // const isSharedConversation = pathname.startsWith('/share/') && window.location.href.endsWith('/continue'); + const conversationId = pathname.split('/').pop().replace(/[^a-z0-9-]/gi, ''); + const anyUserMessageWrappers = document.querySelectorAll('[id^="message-wrapper-"][data-role="user"]').length > 0; + if (/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(conversationId) && anyUserMessageWrappers) { + chrome.storage.local.get(['conversations', 'settings', 'models']).then((res) => { + const { conversations, settings, models } = res; + const conversation = conversations[conversationId]; + chrome.storage.sync.get(['name', 'avatar'], (result) => { + let text = textAreaElement.value.trim(); + if (chunkNumber === 1) { + finalSummary = ''; + if (settings.autoSplit && text.length > settings.autoSplitLimit) { + totalChunks = Math.ceil(text.length / settings.autoSplitLimit); + const lastNewLineIndexBeforeLimit = settings.autoSplitLimit > text.length ? settings.autoSplitLimit : getLastIndexOf(text, settings.autoSplitLimit); + remainingText = text.substring(lastNewLineIndexBeforeLimit); + text = `${settings.autoSplitInitialPrompt}[START CHUNK ${chunkNumber}/${totalChunks}] ${text.substring(0, lastNewLineIndexBeforeLimit)} [END CHUNK ${chunkNumber}/${totalChunks}] ${settings.autoSplitChunkPrompt}`; - chunkNumber += 1; + chunkNumber += 1; + } else { + text = generateInstructions(conversation, settings, textAreaElement.value.trim()); + } + } else if (chunkNumber === totalChunks) { + if (totalChunks > 1 && settings.autoSummarize) shouldSubmitFinalSummary = true; + chunkNumber = 1; + totalChunks = 1; + remainingText = ''; } else { - text = generateInstructions(conversation, settings, textAreaElement.value.trim()); + chunkNumber += 1; + const lastNewLineIndexBeforeLimit = settings.autoSplitLimit > remainingText.length ? settings.autoSplitLimit : getLastIndexOf(remainingText, settings.autoSplitLimit); + remainingText = remainingText.slice(lastNewLineIndexBeforeLimit); } - } else if (chunkNumber === totalChunks) { - if (totalChunks > 1 && settings.autoSummarize) shouldSubmitFinalSummary = true; - chunkNumber = 1; - totalChunks = 1; - remainingText = ''; - } else { - chunkNumber += 1; - const lastNewLineIndexBeforeLimit = settings.autoSplitLimit > remainingText.length ? settings.autoSplitLimit : getLastIndexOf(remainingText, settings.autoSplitLimit); - remainingText = remainingText.slice(lastNewLineIndexBeforeLimit); - } - const messageId = self.crypto.randomUUID(); - const allMessages = document.querySelectorAll('[id^="message-wrapper-"]'); - const lastMessage = allMessages[allMessages.length - 1]; - const parentId = lastMessage?.id?.split('message-wrapper-')[1] || self.crypto.randomUUID(); - const conversationBottom = document.querySelector('#conversation-bottom'); - const node = { message: { id: messageId, content: { parts: [text] } } }; - const userRow = rowUser(conversation, node, 1, 1, result.name, result.avatar, settings.customConversationWidth, settings.conversationWidth); - conversationBottom.insertAdjacentHTML('beforebegin', userRow); - conversationBottom.scrollIntoView({ behavior: 'smooth' }); - if (text) { - isGenerating = true; - submitChat(text, conversation, messageId, parentId, settings, models); - textAreaElement.value = ''; - updateInputCounter(''); - } + const messageId = self.crypto.randomUUID(); + const allMessages = document.querySelectorAll('[id^="message-wrapper-"]'); + const lastMessage = allMessages[allMessages.length - 1]; + const parentId = lastMessage?.id?.split('message-wrapper-')[1] || self.crypto.randomUUID(); + const conversationBottom = document.querySelector('#conversation-bottom'); + if (text && settings.useCustomInstruction) { + text += settings.customInstruction; + } + const node = { message: { id: messageId, content: { parts: [text] } } }; + const userRow = rowUser(conversation, node, 1, 1, result.name, result.avatar, settings.customConversationWidth, settings.conversationWidth); + conversationBottom.insertAdjacentHTML('beforebegin', userRow); + conversationBottom.scrollIntoView({ behavior: 'smooth' }); + if (text) { + isGenerating = true; + submitChat(text, conversation, messageId, parentId, settings, models); + textAreaElement.value = ''; + updateInputCounter(''); + } + }); }); - }); - } else { - chrome.storage.local.get(['settings', 'models']).then((res) => { - const { settings, models } = res; - chrome.storage.sync.get(['name', 'avatar'], (result) => { - let text = textAreaElement.value.trim(); - if (chunkNumber === 1) { - finalSummary = ''; - if (settings.autoSplit && text.length > settings.autoSplitLimit) { - totalChunks = Math.ceil(text.length / settings.autoSplitLimit); - const lastNewLineIndexBeforeLimit = settings.autoSplitLimit > text.length ? settings.autoSplitLimit : getLastIndexOf(text, settings.autoSplitLimit); - remainingText = text.substring(lastNewLineIndexBeforeLimit); - text = `${settings.autoSplitInitialPrompt}[START CHUNK ${chunkNumber}/${totalChunks}] + } else { + chrome.storage.local.get(['settings', 'models']).then((res) => { + const { settings, models } = res; + chrome.storage.sync.get(['name', 'avatar'], (result) => { + let text = textAreaElement.value.trim(); + if (chunkNumber === 1) { + finalSummary = ''; + if (settings.autoSplit && text.length > settings.autoSplitLimit) { + totalChunks = Math.ceil(text.length / settings.autoSplitLimit); + const lastNewLineIndexBeforeLimit = settings.autoSplitLimit > text.length ? settings.autoSplitLimit : getLastIndexOf(text, settings.autoSplitLimit); + remainingText = text.substring(lastNewLineIndexBeforeLimit); + text = `${settings.autoSplitInitialPrompt}[START CHUNK ${chunkNumber}/${totalChunks}] ${text.substring(0, lastNewLineIndexBeforeLimit)} [END CHUNK ${chunkNumber}/${totalChunks}] ${settings.autoSplitChunkPrompt}`; - chunkNumber += 1; + chunkNumber += 1; + } else { + text = generateInstructions({}, settings, textAreaElement.value.trim()); + } + } else if (chunkNumber === totalChunks) { + if (totalChunks > 1 && settings.autoSummarize) shouldSubmitFinalSummary = true; + chunkNumber = 1; + totalChunks = 1; + remainingText = ''; } else { - text = generateInstructions({}, settings, textAreaElement.value.trim()); + chunkNumber += 1; + const lastNewLineIndexBeforeLimit = settings.autoSplitLimit > remainingText.length ? settings.autoSplitLimit : getLastIndexOf(remainingText, settings.autoSplitLimit); + remainingText = remainingText.slice(lastNewLineIndexBeforeLimit); } - } else if (chunkNumber === totalChunks) { - if (totalChunks > 1 && settings.autoSummarize) shouldSubmitFinalSummary = true; - chunkNumber = 1; - totalChunks = 1; - remainingText = ''; - } else { - chunkNumber += 1; - const lastNewLineIndexBeforeLimit = settings.autoSplitLimit > remainingText.length ? settings.autoSplitLimit : getLastIndexOf(remainingText, settings.autoSplitLimit); - remainingText = remainingText.slice(lastNewLineIndexBeforeLimit); - } - const messageId = self.crypto.randomUUID(); - const node = { message: { id: messageId, content: { parts: [text] } } }; - const allMessages = document.querySelectorAll('[id^="message-wrapper-"]'); - const lastMessage = allMessages[allMessages.length - 1]; - const parentId = lastMessage?.id?.split('message-wrapper-')[1] || self.crypto.randomUUID(); - // remove main first child - const contentWrapper = main.querySelector('.flex-1.overflow-hidden'); - main.removeChild(contentWrapper); - - const outerDiv = document.createElement('div'); - outerDiv.classList = 'flex-1 overflow-hidden'; - const innerDiv = document.createElement('div'); - innerDiv.classList = 'h-full overflow-y-auto'; - innerDiv.style = 'scroll-behavior: smooth;'; - innerDiv.id = 'conversation-inner-div'; - addScrollDetector(innerDiv); - const conversationDiv = document.createElement('div'); - conversationDiv.classList = 'flex flex-col items-center text-sm h-full dark:bg-gray-800'; - const userRow = rowUser({}, node, 1, 1, result.name, result.avatar, settings.customConversationWidth, settings.conversationWidth); - conversationDiv.innerHTML = userRow; - const topDiv = '
New chat
'; - conversationDiv.insertAdjacentHTML('afterbegin', topDiv); - const bottomDiv = document.createElement('div'); - bottomDiv.id = 'conversation-bottom'; - bottomDiv.classList = 'w-full h-32 md:h-48 flex-shrink-0'; - conversationDiv.appendChild(bottomDiv); - const bottomDivContent = document.createElement('div'); - bottomDivContent.classList = 'relative text-base gap-4 md:gap-6 m-auto md:max-w-2xl lg:max-w-2xl xl:max-w-3xl flex lg:px-0'; - if (settings.customConversationWidth) { - bottomDivContent.style = `max-width: ${settings.conversationWidth}%`; - } - bottomDiv.appendChild(bottomDivContent); - const totalCounter = document.createElement('div'); - totalCounter.id = 'total-counter'; - totalCounter.style = 'position: absolute; top: 0px; right: 0px; font-size: 10px; color: rgb(153, 153, 153); opacity: 0.8;'; - bottomDivContent.appendChild(totalCounter); - - innerDiv.appendChild(conversationDiv); - outerDiv.appendChild(innerDiv); - main.prepend(outerDiv); - if (text) { - isGenerating = true; - submitChat(text, {}, messageId, parentId, settings, models); - textAreaElement.value = ''; - updateInputCounter(''); - } + const messageId = self.crypto.randomUUID(); + if (text && settings.useCustomInstruction) { + text += settings.customInstruction; + } + const node = { message: { id: messageId, content: { parts: [text] } } }; + const allMessages = document.querySelectorAll('[id^="message-wrapper-"]'); + const lastMessage = allMessages[allMessages.length - 1]; + const parentId = lastMessage?.id?.split('message-wrapper-')[1] || self.crypto.randomUUID(); + // remove main first child + const contentWrapper = main.querySelector('.flex-1.overflow-hidden'); + main.removeChild(contentWrapper); + + const outerDiv = document.createElement('div'); + outerDiv.classList = 'flex-1 overflow-hidden'; + const innerDiv = document.createElement('div'); + innerDiv.classList = 'h-full overflow-y-auto'; + innerDiv.style = 'scroll-behavior: smooth;'; + innerDiv.id = 'conversation-inner-div'; + addScrollDetector(innerDiv); + const conversationDiv = document.createElement('div'); + conversationDiv.classList = 'flex flex-col items-center text-sm h-full dark:bg-gray-800'; + const userRow = rowUser({}, node, 1, 1, result.name, result.avatar, settings.customConversationWidth, settings.conversationWidth); + conversationDiv.innerHTML = userRow; + const topDiv = '
New chat
'; + conversationDiv.insertAdjacentHTML('afterbegin', topDiv); + const bottomDiv = document.createElement('div'); + bottomDiv.id = 'conversation-bottom'; + bottomDiv.classList = 'w-full h-32 md:h-48 flex-shrink-0'; + conversationDiv.appendChild(bottomDiv); + const bottomDivContent = document.createElement('div'); + bottomDivContent.classList = 'relative text-base gap-4 md:gap-6 m-auto md:max-w-2xl lg:max-w-2xl xl:max-w-3xl flex lg:px-0'; + if (settings.customConversationWidth) { + bottomDivContent.style = `max-width: ${settings.conversationWidth}%`; + } + bottomDiv.appendChild(bottomDivContent); + const totalCounter = document.createElement('div'); + totalCounter.id = 'total-counter'; + totalCounter.style = 'position: absolute; top: 0px; right: 0px; font-size: 10px; color: rgb(153, 153, 153); opacity: 0.8;'; + bottomDivContent.appendChild(totalCounter); + + innerDiv.appendChild(conversationDiv); + outerDiv.appendChild(innerDiv); + main.prepend(outerDiv); + if (text) { + isGenerating = true; + submitChat(text, {}, messageId, parentId, settings, models); + textAreaElement.value = ''; + updateInputCounter(''); + } + }); }); - }); + } } }); // textAreaElement.addEventListener('keydown', (e) => { @@ -917,10 +945,23 @@ ${settings.autoSplitChunkPrompt}`; submitButtonClone.addEventListener('click', () => { const textAreaElement = inputForm.querySelector('textarea'); if (isGenerating) return; - if (textAreaElement.value.trim().length === 0) return; - textAreaElement.style.height = '24px'; - addUserPromptToHistory(textAreaElement.value.trim()); - inputForm.dispatchEvent(new Event('submit', { cancelable: true })); + const templateWords = textAreaElement.value.match(/{{(.*?)}}/g); + if (templateWords?.length > 0) { + // open template words modal and wait for user to select a word. the when user submit, submit the input form with the replacement + createTemplateWordsModal(templateWords); + const firstTemplateWordInput = document.querySelector('[id^=template-input-]'); + if (firstTemplateWordInput) { + firstTemplateWordInput.focus(); + setTimeout(() => { + firstTemplateWordInput.value = ''; + }, 100); + } + } else { + if (textAreaElement.value.trim().length === 0) return; + textAreaElement.style.height = '24px'; + addUserPromptToHistory(textAreaElement.value.trim()); + inputForm.dispatchEvent(new Event('submit', { cancelable: true })); + } }); submitButton.parentNode.replaceChild(submitButtonClone, submitButton); } diff --git a/scripts/content/global.js b/scripts/content/global.js index 4c9c28d..8380b35 100644 --- a/scripts/content/global.js +++ b/scripts/content/global.js @@ -451,7 +451,8 @@ function addGpt4Counter() { function updateInputCounter(text) { const curInputCounterElement = document.querySelector('#gptx-input-counter'); if (curInputCounterElement) { - const wordCount = text ? text.split(' ').length : 0; + // word count split by space or newline + const wordCount = text ? text.split(/[\s\n]+/).length : 0; const charCount = text.length; if (charCount < 16000) { curInputCounterElement.style.color = '#999'; diff --git a/scripts/content/pluginStore.js b/scripts/content/pluginStore.js index 81a0dda..361df98 100644 --- a/scripts/content/pluginStore.js +++ b/scripts/content/pluginStore.js @@ -11,7 +11,7 @@ function initializePluginStoreModal(plugins) { style="pointer-events: auto;" >