Skip to content

Commit 4b493df

Browse files
committed
1 parent 49c6c9a commit 4b493df

1 file changed

Lines changed: 1 addition & 221 deletions

File tree

chatgpt/duckduckgpt/duckduckgpt.user.js

Lines changed: 1 addition & 221 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
// @description:zu Yengeza izimpendulo ze-AI ku-DuckDuckGo (inikwa amandla yi-GPT-4o!)
149149
// @author KudoAI
150150
// @namespace https://kudoai.com
151-
// @version 2026.1.3
151+
// @version 2026.1.3.1
152152
// @license MIT
153153
// @icon https://assets.ddgpt.com/images/icons/app/icon48.png?v=533ce0f
154154
// @icon64 https://assets.ddgpt.com/images/icons/app/icon64.png?v=533ce0f
@@ -254,226 +254,6 @@
254254

255255
// Requires components/replyBubble.js + lib/<dom|Tone>.js + <app|get|prompts|show|tooltip|xhr>
256256

257-
window.buttons = {
258-
reply: {
259-
bubble: {
260-
types: ['copy', 'share', 'regen', 'speak'], // right-to-left
261-
styles: 'float: right ; cursor: pointer ;',
262-
263-
create() { // requires lib/<dom|Tone>.js + <app|get|prompts|show|tooltip|xhr>
264-
if (this.share) return
265-
266-
// Copy button
267-
this.copy = dom.create.elem('btn', {
268-
id: `${app.slug}-copy-btn`, class: 'no-mobile-tap-outline',
269-
style: this.styles + 'display: flex ; margin-top: 1.5px'
270-
})
271-
const copySVGs = {
272-
copy: icons.create({ key: 'copy' }), copied: icons.create({ key: 'checkmarkDouble' })}
273-
Object.entries(copySVGs).forEach(([svgType, svg]) => {
274-
svg.id = `${app.slug}-${svgType}-icon`
275-
;['width', 'height'].forEach(attr => svg.setAttribute(attr, 13))
276-
})
277-
this.copy.append(copySVGs.copy)
278-
this.copy.listeners = {}
279-
if (!env.browser.isMobile) // store/add tooltip listeners
280-
['onmouseenter', 'onmouseleave'].forEach(eventType =>
281-
this.copy[eventType] = this.copy.listeners[eventType] = tooltip.toggle)
282-
this.copy.listeners.onclick = this.copy.onclick = ({ currentTarget }) => { // copy text, update icon + tooltip status
283-
const copyBtn = currentTarget
284-
if (!copyBtn.firstChild.matches('[id$=copy-icon]')) return // since clicking on Copied icon
285-
const textContainer = (
286-
currentTarget.parentNode.className.includes('reply-header')
287-
? app.div.querySelector('.reply-pre') // reply container
288-
: currentTarget.closest('code') // code container
289-
)
290-
const textToCopy = textContainer.textContent.replace(/^>> /, '').trim()
291-
copyBtn.style.cursor = 'default' // remove finger
292-
copyBtn.firstChild.replaceWith(copySVGs.copied.cloneNode(true)) // change to Copied icon
293-
tooltip.update(currentTarget) // to 'Copied to clipboard!'
294-
setTimeout(() => { // restore icon/cursor/tooltip after a bit
295-
copyBtn.firstChild.replaceWith(copySVGs.copy.cloneNode(true))
296-
copyBtn.style.cursor = 'pointer'
297-
if (copyBtn.matches(':hover')) // restore tooltip
298-
copyBtn.dispatchEvent(new Event('mouseenter'))
299-
}, 1355)
300-
navigator.clipboard.writeText(textToCopy) // copy text to clipboard
301-
}
302-
303-
// Share button
304-
this.share = dom.create.elem('btn', {
305-
id: `${app.slug}-share-btn`, class: 'no-mobile-tap-outline',
306-
style: this.styles + 'margin-right: 10px ;' + (
307-
app.slug == 'bravegpt' ? 'position: relative ; bottom: 2px' : '' )
308-
})
309-
this.share.append(icons.create({ key: 'arrowShare', size: 16 }))
310-
if (!env.browser.isMobile) this.share.onmouseenter = this.share.onmouseleave = tooltip.toggle
311-
this.share.onclick = ({ currentTarget }) => {
312-
if (show.reply.shareURL) return modals.shareChat(show.reply.shareURL)
313-
this.share.style.cursor = 'default' // remove finger
314-
if (!config.fgAnimationsDisabled) this.share.style.animation = 'spinY 1s linear infinite'
315-
tooltip.update(currentTarget) // to 'Generating HTML...'
316-
xhr({
317-
method: 'POST', url: app.urls.apis.chatShare,
318-
headers: { 'Content-Type': 'application/json', 'Referer': location.href },
319-
data: JSON.stringify({ messages: prompts.stripAugments(app.msgChain) }),
320-
onload: ({ responseText }) => {
321-
modals.shareChat(show.reply.shareURL = JSON.parse(responseText).url)
322-
this.share.style.animation = '' ; this.share.style.cursor = 'pointer'
323-
}
324-
})
325-
}
326-
327-
// Regenerate button
328-
this.regen = dom.create.elem('btn', {
329-
id: `${app.slug}-regen-btn`, class: 'no-mobile-tap-outline',
330-
style: this.styles + 'position: relative ; top: 1px ; margin: 0 9px 0 5px'
331-
})
332-
const regenSVGwrapper = dom.create.elem('div', { // to spin while respecting ini icon tilt
333-
style: 'display: flex' }) // wrap the icon tightly
334-
regenSVGwrapper.append(icons.create({ key: 'arrowsCyclic', size: 14 }))
335-
this.regen.append(regenSVGwrapper)
336-
if (!env.browser.isMobile) this.regen.onmouseenter = this.regen.onmouseleave = tooltip.toggle
337-
this.regen.onclick = ({ currentTarget }) => {
338-
get.reply({ msgs: app.msgChain, src: 'regen' })
339-
regenSVGwrapper.style.cursor = 'default' // remove finger
340-
if (config.fgAnimationsDisabled) regenSVGwrapper.style.transform = 'rotate(90deg)'
341-
else regenSVGwrapper.style.animation = 'rotate 1s infinite cubic-bezier(0, 1.05, 0.79, 0.44)'
342-
tooltip.update(currentTarget) // to 'Regenerating...'
343-
show.reply.chatbarFocused = false ; show.reply.userInteracted = true
344-
}
345-
346-
// Speak button
347-
this.speak = dom.create.elem('btn', {
348-
id: `${app.slug}-speak-btn`, class: 'no-mobile-tap-outline',
349-
style: this.styles + 'margin: -1px 3px 0 0'
350-
})
351-
const speakSVGwrapper = dom.create.elem('div', { // to show 1 icon at a time during scroll
352-
style: 'width: 19px ; height: 19px ; overflow: hidden' })
353-
const speakSVGscroller = dom.create.elem('div', { // to scroll the icons
354-
style: `display: flex ; /* align the SVGs horizontally */
355-
width: 41px ; height: 22px /* rectangle to fit both icons */` })
356-
const speakSVGs = { speak: icons.create({ key: 'soundwave', id: `${app.slug}-speak-icon` })}
357-
;['generating', 'playing'].forEach(state => {
358-
speakSVGs[state] = []
359-
for (let i = 0 ; i < 2 ; i++) { // push/id 2 of each state icon for continuous scroll animation
360-
speakSVGs[state].push(
361-
icons.create({ key: `soundwave${ state == 'generating' ? 'Short' : 'Tall' }`}))
362-
speakSVGs[state][i].id = `${app.slug}-${state}-icon-${i+1}`
363-
if (i == 1) // close gap of 2nd icon during scroll
364-
speakSVGs[state][i].style.marginLeft = `-${ state == 'generating' ? 3 : 5 }px`
365-
}
366-
})
367-
speakSVGscroller.append(speakSVGs.speak) ; speakSVGwrapper.append(speakSVGscroller)
368-
this.speak.append(speakSVGwrapper)
369-
if (!env.browser.isMobile) this.speak.onmouseenter = this.speak.onmouseleave = tooltip.toggle
370-
this.speak.onclick = async ({ currentTarget }) => {
371-
if (this.speak.contains(speakSVGs.generating[0])) return
372-
if (window.currentlyPlayingAudio) {
373-
window.currentlyPlayingAudio.stop() ; handleAudioEnded() ; return }
374-
375-
this.speak.style.cursor = 'default' // remove finger
376-
377-
// Update icon to Generating ones
378-
speakSVGscroller.textContent = '' // rid Speak icon
379-
speakSVGscroller.append(speakSVGs.generating[0], speakSVGs.generating[1]) // add Generating icons
380-
if (!config.fgAnimationsDisabled) { // animate icons
381-
speakSVGscroller.style.animation = 'icon-scroll 1s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite'
382-
speakSVGwrapper.style.maskImage = ( // fade edges
383-
'linear-gradient(to right, transparent, black 20%, black 81%, transparent)' )
384-
}
385-
386-
tooltip.update(currentTarget) // to 'Generating audio...'
387-
388-
// Init Sogou TTS dialect map
389-
window.sgtDialectMap ||= await get.json(`${app.urls.aiwebAssets}/data/sogou-tts-lang-codes.json`)
390-
.catch(err => log.error(err.message)) ; if (!window.sgtDialectMap) return
391-
Object.entries(window.sgtDialectMap).forEach(([sgtCode, langData]) => {
392-
langData.isoOrNamePattern = new RegExp(langData.isoOrNamePattern, 'i')
393-
langData.sgtCode = sgtCode
394-
})
395-
396-
// Init other config/data
397-
const wholeAnswer = app.div.querySelector('.reply-pre').textContent
398-
const cjsSpeakConfig = { voice: 2, pitch: 1, speed: 1.5, onend: handleAudioEnded }
399-
const sgtDialectData = Object.values(window.sgtDialectMap).find(langData =>
400-
langData.isoOrNamePattern.test(config.replyLang)
401-
) || window.sgtDialectMap.en
402-
const payload = { text: wholeAnswer, curTime: Date.now(), spokenDialect: sgtDialectData.sgtCode }
403-
const key = CryptoJS.enc.Utf8.parse('76350b1840ff9832eb6244ac6d444366')
404-
const iv = CryptoJS.enc.Utf8.parse(
405-
atob('AAAAAAAAAAAAAAAAAAAAAA==') || '76350b1840ff9832eb6244ac6d444366')
406-
const securePayload = CryptoJS.AES.encrypt(JSON.stringify(payload), key, {
407-
iv, mode: CryptoJS.mode.CBC, pad: CryptoJS.pad.Pkcs7 }).toString()
408-
409-
// Play reply
410-
xhr({
411-
url: 'https://fanyi.sogou.com/openapi/external/getWebTTS?S-AppId=102356845&S-Param='
412-
+ encodeURIComponent(securePayload),
413-
method: 'GET', responseType: 'arraybuffer',
414-
onload: resp => {
415-
416-
// Update icons to Playing ones
417-
speakSVGscroller.textContent = '' // rid Generating icons
418-
speakSVGscroller.append(speakSVGs.playing[0], speakSVGs.playing[1]) // add Playing icons
419-
if (!config.fgAnimationsDisabled) // animate icons
420-
speakSVGscroller.style.animation = 'icon-scroll 0.5s linear infinite'
421-
422-
if (this.speak.matches(':hover')) // restore tooltip
423-
this.speak.dispatchEvent(new Event('mouseenter'))
424-
425-
// Play audio
426-
if (resp.status != 200) {
427-
buttons.reply.bubble.speak.style.cursor = 'pointer'
428-
window.currentlyPlayingAudio = chatgpt.speak(wholeAnswer, cjsSpeakConfig)
429-
} else {
430-
const audioContext = new (window.webkitAudioContext || window.AudioContext)()
431-
audioContext.decodeAudioData(resp.response, buffer => {
432-
buttons.reply.bubble.speak.style.cursor = 'pointer'
433-
const player = new Tone.Player(buffer), speed = 1.5
434-
const fx = { // applied in top-down order
435-
pitchShifter: new Tone.PitchShift(12 * Math.log2(1/speed)), // keep og pitch
436-
phaser: new Tone.Phaser({ // make robotic
437-
frequency: 55, octaves: 5, baseFrequency: 1000 }),
438-
chorus: new Tone.Chorus(4, 2.5, 0.5),
439-
eq: new Tone.EQ3({ // apply smile curve
440-
low: 12, mid: 0, high: 9, lowFrequency: 300, highFrequency: 500 })
441-
}
442-
let outputNode = player
443-
Object.values(fx).forEach(effect => { // chain 'em
444-
outputNode.connect(effect) ; outputNode = effect })
445-
outputNode.toDestination()
446-
player.playbackRate = speed ; player.start()
447-
window.currentlyPlayingAudio = player ; player.onstop = handleAudioEnded
448-
}).catch(() => {
449-
buttons.reply.bubble.speak.style.cursor = 'pointer'
450-
window.currentlyPlayingAudio = chatgpt.speak(wholeAnswer, cjsSpeakConfig)
451-
})
452-
}
453-
}
454-
})
455-
456-
function handleAudioEnded() {
457-
speakSVGscroller.textContent = speakSVGscroller.style.animation = '' // rid Playing icons
458-
speakSVGscroller.append(speakSVGs.speak) // restore Speak icon
459-
if (buttons.reply.bubble.speak.matches(':hover')) // restore tooltip
460-
buttons.reply.bubble.speak.dispatchEvent(new Event('mouseenter'))
461-
window.currentlyPlayingAudio = null
462-
}
463-
}
464-
},
465-
466-
insert() { // requires components/replyBubble.js + lib/dom.js
467-
if (!this.share) this.create() ; if (!replyBubble.preHeader) replyBubble.create()
468-
replyBubble.preHeader.append(
469-
this.preHeaderBtnsDiv ||= dom.create.elem('div', { class: 'reply-header-btns' }))
470-
this.preHeaderBtnsDiv.append(this.copy, this.share, this.regen, this.speak)
471-
}
472-
}
473-
}
474-
};
475-
476-
477257
(async () => {
478258
'use strict'
479259

0 commit comments

Comments
 (0)