forked from mohalobaidi/EnhancedChatGPT
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinject.js
414 lines (362 loc) · 17.2 KB
/
inject.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
(() => {
// Save a reference to the original fetch function
const fetch = window._fetch = window._fetch || window.fetch
// Replace the fetch function with a modified version that will include a prompt template
// if one has been selected by the user
window.fetch = (...t) => {
// If the request is not for the chat backend API, just use the original fetch function
if (t[0] !== 'https://chat.openai.com/backend-api/conversation') return fetch(...t)
// If no prompt template has been selected, use the original fetch function
if (!window.selectedprompttemplate) return fetch(...t)
// Get the selected prompt template
const template = window.selectedprompttemplate
try {
// Get the options object for the request, which includes the request body
const options = t[1]
// Parse the request body from JSON
const body = JSON.parse(options.body)
// Get the prompt from the request body
const prompt = body.messages[0].content.parts[0]
// Replace the prompt in the request body with the selected prompt template,
// inserting the original prompt into the template
body.messages[0].content.parts[0] = template.prompt.replace('[INSERT]', prompt)
// Clear the selected prompt template
selectPromptTemplate(null)
// Stringify the modified request body and update the options object
options.body = JSON.stringify(body)
// Use the modified fetch function to make the request
return fetch(t[0], options)
} catch {
// If there was an error parsing the request body or modifying the request,
// just use the original fetch function
return fetch(...t)
}
}
// Create a new observer for the chat sidebar to watch for changes to the document body
const observer = new MutationObserver(mutations => {
// For each mutation (change) to the document body
mutations.forEach(mutation => {
// If the mutation is not a change to the list of child nodes, skip it
if (mutation.type !== 'childList')
// If no new nodes were added, skip this mutation
if (mutation.addedNodes.length == 0) return
// Get the first added node
const node = mutation.addedNodes[0]
// If the node is not an element or does not have a `querySelector` method, skip it
if (!node || !node.querySelector) return
// Call the `handleElementAdded` function with the added node
handleElementAdded(node)
})
})
// Start observing the document body for changes
observer.observe(document.body, { subtree: true, childList: true })
// Fetch the list of prompt templates from a remote CSV file
fetch('https://raw.githubusercontent.com/mohalobaidi/awesome-chatgpt-prompts/main/prompts.csv')
// Convert the response to text
.then(res => res.text())
// Convert the CSV text to an array of records
.then(csv => CSVToArray(csv))
// Map the records to template objects with properties 'title', 'prompt', and 'placeholder'
.then(records => {
return records.map(([ title, prompt, placeholder ]) => {
return { title, prompt, placeholder }
})
// Filter out records that do not have a title or it is the header row (with "title" as its title)
.filter(({ title }) => title && title !== 'title')
})
.then(templates => {
// Save the array of prompt templates to a global variable
window.prompttemplates = templates
// Insert the "Prompt Templates" section into the chat interfac
insertPromptTemplatesSection()
})
// Set up the Sidebar (by adding "Export Chat" button and other stuff)
setupSidebar()
})()
// This function is called for each new element added to the document body
function handleElementAdded (e) {
// If the element added is the root element for the chat sidebar, set up the sidebar
if (e.id === 'headlessui-portal-root') {
setupSidebar()
return
}
// Disable "Export Button" when no chat were started.
// Insert "Prompt Templates" section to the main page.
if (e.querySelector('h1.text-4xl')) {
insertPromptTemplatesSection()
const button = document.getElementById('export-button')
if (button) button.style = 'pointer-events: none;opacity: 0.5'
}
// Enable "Export Button" when a new chat started.
if (document.querySelector('.xl\\:max-w-3xl')) {
const button = document.getElementById('export-button')
if (button) button.style = ''
}
// Add "Copy Button" to Assistant's chat bubble.
if (e.querySelector('.lg\\:self-center.lg\\:pl-2')) {
// Get buttons group
const buttonGroup = e.querySelector('.lg\\:self-center.lg\\:pl-2')
// Filter out Assistant's chat bubble from User's chat bubble
if (buttonGroup.children.length !== 2) return
// It heavily depends on the fact Assistant's has two buttons, "upvote" and "downvote".
// and the user has only one button, "edit prompt".
addCopyButton(buttonGroup)
}
}
// This function sets up the chat sidebar by adding an "Export Button" and modifying
// the "New Chat" buttons to clear the selected prompt template when clicked
function setupSidebar () {
// Add the "Export Button" to the sidebar
addExportButton()
// Get the "New Chat" buttons
const buttons = getNewChatButtons()
// Set the onclick event for each button to clear the selected prompt template
buttons.forEach(button => {
button.onclick = () => {
selectPromptTemplate(null)
}
})
}
// This function adds an "Export Button" to the sidebar
function addExportButton () {
// Get the nav element in the sidebar
const nav = document.querySelector('nav')
// If there is no nav element or the "Export Button" already exists, skip
if (!nav || nav.querySelector('#export-button')) return
// Create the "Export Button" element
const button = document.createElement('a')
button.id = 'export-button'
button.className = css`ExportButton`
button.innerHTML = `${svg`Archive`} Export Chat`
button.onclick = exportCurrentChat
// If there is no chat started, disable the button
if (document.querySelector('.flex-1.overflow-hidden h1')) {
button.style = 'pointer-events: none;opacity: 0.5'
}
// Get the "Dark Mode" and "Light Mode" button as a reference point
const colorModeButton = [...nav.children].find(child => child.innerText.includes('Mode'))
// Insert the "Export Button" before the "Color Mode" button
nav.insertBefore(button, colorModeButton)
}
// This function gets the "New Chat" buttons
function getNewChatButtons (callback) {
// Get the sidebar and topbar elements
const sidebar = document.querySelector('nav')
const topbar = document.querySelector('.sticky')
// Get the "New Chat" button in the sidebar
const newChatButton = [...sidebar?.querySelectorAll('.cursor-pointer') ?? []].find(e => e.innerText === 'New Chat')
// Get the "Plus" button in the topbar
const AddButton = topbar?.querySelector("button.px-3")
// Return an array containing the buttons, filtering out any null elements
return [newChatButton, AddButton].filter(button => button)
}
// This object contains properties for the prompt templates section
const promptTemplateSection = {
currentPage: 0, // The current page number
pageSize: 5 // The number of prompt templates per page
}
// This function inserts a section containing a list of prompt templates into the chat interface
function insertPromptTemplatesSection () {
// Get the title element (as a reference point and also for some alteration)
const title = document.querySelector('h1.text-4xl')
// If there is no title element, return
if (!title) return
// Style the title element and set it to "Enhanced ChatGPT"
title.style = 'text-align: center; margin-top: 4rem'
title.innerHTML = 'Enhanced ChatGPT'
// Get the list of prompt templates
const templates = window.prompttemplates
// If there are no templates, skip
if (!templates) return
// Get the parent element of the title element (main page)
const parent = title.parentElement
// If there is no parent element, skip
if (!parent) return
// Remove the "md:h-full" class from the parent element
parent.classList.remove('md:h-full')
// Get the current page number and page size from the promptTemplateSection object
const { currentPage, pageSize } = promptTemplateSection
// Calculate the start and end indices of the current page of prompt templates
const start = pageSize * currentPage
const end = Math.min(pageSize * (currentPage + 1), templates.length)
// Get the current page of prompt templates
const currentTemplates = window.prompttemplates.slice(start, end)
// Create the HTML for the prompt templates section
const html = `
<div class="${css`column`}">
${svg`ChatBubble`}
<h2 class="${css`h2`}">
<ul class="${css`ul`}">
${currentTemplates.map((template, i) => `
<button onclick="selectPromptTemplate(${start + i})" class="${css`card`}">
<h3 class="${css`h3`}">${template.title}</h3>
<p class="${css`p`}">${
template.prompt.replace('[INSERT]', template.placeholder)
}</p>
<span class="font-medium">Use prompt →</span>
</button>
`).join('')}
</ul>
<div class="${css`column`} items-center">
<span class="${css`paginationText`}">
Showing <span class="${css`paginationNumber`}">${start + 1}</span> to <span class="${css`paginationNumber`}">${end}</span> of <a href="https://prompts.chat/" target="_blank" class="underline"><span class="${css`paginationNumber`}">${templates.length} Entries</span></a>
</span>
<div class="${css`paginationButtonGroup`}">
<button onclick="prevPromptTemplatesPage()" class="${css`paginationButton`}" style="border-radius: 6px 0 0 6px">Prev</button>
<button onclick="nextPromptTemplatesPage()" class="${css`paginationButton`} border-0 border-l border-gray-500" style="border-radius: 0 6px 6px 0">Next</button>
</div>
</div>
</div>
`
let wrapper = document.createElement('div')
wrapper.id = 'templates-wrapper'
wrapper.className = 'mt-6 flex items-start text-center gap-3.5'
if (parent.querySelector('#templates-wrapper')) {
wrapper = parent.querySelector('#templates-wrapper')
} else {
parent.appendChild(wrapper)
}
wrapper.innerHTML = html
}
function prevPromptTemplatesPage () {
promptTemplateSection.currentPage--
promptTemplateSection.currentPage = Math.max(0, promptTemplateSection.currentPage)
// Update the section
insertPromptTemplatesSection()
}
function nextPromptTemplatesPage () {
const templates = window.prompttemplates
if (!templates || !Array.isArray(templates)) return
promptTemplateSection.currentPage++
promptTemplateSection.currentPage = Math.min(
Math.floor(
(templates.length - 1) /
promptTemplateSection.pageSize
),
promptTemplateSection.currentPage
)
// Update the section
insertPromptTemplatesSection()
}
function addCopyButton (buttonGroup) {
const button = document.createElement('button')
button.onclick = () => {
const text = buttonGroup.parentElement.innerText
navigator.clipboard.writeText(text)
}
button.className = css`action`
button.innerHTML = svg`Clipboard`
buttonGroup.prepend(button)
}
function exportCurrentChat () {
const blocks = [...document.querySelector('.flex.flex-col.items-center').children]
let markdown = blocks.map(block => {
let wrapper = block.querySelector('.whitespace-pre-wrap')
if (!wrapper) {
return ''
}
// probably a user's, so..
if (wrapper.children.length === 0) {
return '**User:**\n' + wrapper.innerText
}
// pass this point is assistant's
wrapper = wrapper.firstChild
return '**ChatGPT:**\n' + [...wrapper.children].map(node => {
switch (node.nodeName) {
case 'PRE': return `\`\`\`${node.getElementsByTagName('code')[0].classList[2].split('-')[1]}\n${node.innerText.replace(/^Copy code/g, '').trim()}\n\`\`\``
default: return `${node.innerHTML}`
}
}).join('\n')
})
markdown = markdown.filter(b => b)
if (!markdown) return false
let signature = ''
try {
signature = `***\n###### _Exported by **${__NEXT_DATA__.props.pageProps.user.name}** on ${new Date().toLocaleString()}_`
} catch {}
const blob = new Blob([markdown.join('\n\n***\n\n') + '\n\n\n' + signature], {type: 'text/plain'})
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = 'chatgpt-thread_' + (new Date().toLocaleString('en-US', { hour12: false }).replace(/[\s/:]/g, '-').replace(',', '')) + '.md'
document.body.appendChild(a)
a.click()
}
// This function selects a prompt template
function selectPromptTemplate (idx) {
// Get the list of prompt templates
const templates = window.prompttemplates
// If there are no templates, skip
if (!templates || !Array.isArray(templates)) return
const template = templates[idx]
const textarea = document.querySelector('textarea')
const parent = textarea.parentElement
let wrapper = document.createElement('div')
wrapper.id = 'prompt-wrapper'
if (parent.querySelector('#prompt-wrapper')) {
wrapper = parent.querySelector('#prompt-wrapper')
} else {
parent.prepend(wrapper)
}
if (template) {
wrapper.innerHTML = `
<span class="${css`tag`}">
${template.title}
</span>
`
textarea.placeholder = template.placeholder
window.selectedprompttemplate = template
textarea.focus()
} else {
wrapper.innerHTML = ``
textarea.placeholder = ''
window.selectedprompttemplate = null
}
}
function CSVToArray(strData, strDelimiter) {
strDelimiter = strDelimiter || ",";
var pattern = new RegExp(
"(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
"(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
"([^\"\\" + strDelimiter + "\\r\\n]*))",
"gi"
);
var data = [[]];
var matches;
while (matches = pattern.exec(strData)) {
var delimiter = matches[1];
if (delimiter.length && delimiter !== strDelimiter) {
data.push([]);
}
var value = matches[2]
? matches[2].replace(new RegExp("\"\"", "g"), "\"")
: matches[3];
data[data.length - 1].push(value);
}
return data;
}
function svg (name) {
name = Array.isArray(name) ? name[0] : name
switch (name) {
case 'Archive': return '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4" height="1em" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m8.25 3v6.75m0 0l-3-3m3 3l3-3M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" /></svg>'
case 'ChatBubble': return '<svg stroke="currentColor" fill="none" stroke-width="1.5" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-6 w-6 m-auto" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" /></svg>'
case 'Clipboard': return '<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>'
}
}
function css (name) {
name = Array.isArray(name) ? name[0] : name
switch (name) {
case 'ExportButton': return 'flex py-3 px-3 items-center gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm'
case 'column': return 'flex flex-col gap-3.5 flex-1'
case 'h2': return 'text-lg font-normal">Prompt Templates</h2><ul class="flex flex-col gap-3.5'
case 'h3': return 'm-0 tracking-tight leading-8 text-gray-900 dark:text-gray-100 text-xl'
case 'ul': return 'flex flex-col gap-3.5'
case 'card': return 'flex flex-col gap-2 w-full bg-gray-50 dark:bg-white/5 p-4 rounded-md hover:bg-gray-200 dark:hover:bg-gray-900 text-left'
case 'p': return 'm-0 font-light text-gray-500'
case 'paginationText': return 'text-sm text-gray-700 dark:text-gray-400'
case 'paginationNumber': return 'font-semibold text-gray-900 dark:text-white'
case 'paginationButtonGroup': return 'inline-flex mt-2 xs:mt-0'
case 'paginationButton': return 'px-4 py-2 font-medium bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-400 dark:hover:text-white'
case 'action': return 'p-1 rounded-md hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible'
case 'tag': return 'inline-flex items-center py-1 px-2 mr-2 mb-2 text-sm font-medium text-white rounded bg-gray-600 whitespace-nowrap'
}
}