|
148 | 148 | // @description:zu Yengeza izimpendulo ze-AI ku-DuckDuckGo (inikwa amandla yi-GPT-4o!) |
149 | 149 | // @author KudoAI |
150 | 150 | // @namespace https://kudoai.com |
151 | | -// @version 2026.1.3 |
| 151 | +// @version 2026.1.3.1 |
152 | 152 | // @license MIT |
153 | 153 | // @icon https://assets.ddgpt.com/images/icons/app/icon48.png?v=533ce0f |
154 | 154 | // @icon64 https://assets.ddgpt.com/images/icons/app/icon64.png?v=533ce0f |
|
254 | 254 |
|
255 | 255 | // Requires components/replyBubble.js + lib/<dom|Tone>.js + <app|get|prompts|show|tooltip|xhr> |
256 | 256 |
|
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 | | - |
477 | 257 | (async () => { |
478 | 258 | 'use strict' |
479 | 259 |
|
|
0 commit comments