Voice Control for ChatGPT

Expands ChatGPT with voice control and read aloud. fork from https://chrome.google.com/webstore/detail/eollffkcakegifhacjnlnegohfdlidhn

当前为 2023-06-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Voice Control for ChatGPT
  3. // @name:zh-CN Voice Control for ChatGPT
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.1.0
  6. // @description Expands ChatGPT with voice control and read aloud. fork from https://chrome.google.com/webstore/detail/eollffkcakegifhacjnlnegohfdlidhn
  7. // @description:zh-cn 扩展 ChatGPT
  8. // @author You
  9. // @match https://chat.openai.com/*
  10. // @match https://chat-shared1.zhile.io/*
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com
  12. // @grant none
  13. // @run-at document-idle
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. // Your code here...
  20. const style = document.createElement("style");
  21. style.innerHTML = `
  22.  
  23. #sai-root {
  24. display: flex;
  25. justify-content: space-between;
  26. align-items: center;
  27. margin-top: 10px;
  28. }
  29.  
  30. #sai-input-wrapper {
  31. position: relative;
  32. cursor: pointer;
  33. background-color: #e02d2d;
  34. animation-name: red-pulsating-color;
  35. animation-duration: 2s;
  36. animation-iteration-count: infinite;
  37. max-width: 75%;
  38. }
  39.  
  40. #sai-input-wrapper:hover {
  41. opacity: 0.7;
  42. }
  43.  
  44. #sai-input-wrapper div.w-full {
  45. padding-right: 35px;
  46. }
  47.  
  48. #sai-input-wrapper div {
  49. display: block;
  50. min-height: 24px;
  51. color: #fff;
  52. }
  53.  
  54. #sai-input-wrapper.is-idle {
  55. background-color: #9a8e81;
  56. animation: none;
  57. }
  58.  
  59. /*.light #sai-input-wrapper.is-idle {
  60. background-color: #7f7a89;
  61. }*/
  62.  
  63. #sai-input-wrapper.is-idle #sai-speech-button {
  64. right: 50%;
  65. margin-right: -13px;
  66. width: 24px;
  67. height: 24px;
  68. top: 12px;
  69. }
  70.  
  71. #sai-input-wrapper.is-idle #sai-speech-button svg {
  72. width: 24px;
  73. height: 24px;
  74. }
  75.  
  76. #sai-speech-button {
  77. position: absolute;
  78. top: 10px;
  79. right: 12px;
  80. width: 18px;
  81. transition: 0.5s;
  82. right: 10px;
  83. user-select: none;
  84. }
  85.  
  86. #sai-speech-button svg {
  87. width: 18px;
  88. height: 18px;
  89. }
  90.  
  91. #sai-input-wrapper.is-idle #sai-cancel-msg {
  92. visibility: hidden;
  93. opacity: 0;
  94. }
  95.  
  96. #sai-button-wrapper {
  97. display: flex;
  98. justify-content: space-between;
  99. flex: 1;
  100. padding: 10px 15px;
  101. background: #eeeeee;
  102. margin-left: 15px;
  103. border-radius: 5px;
  104. z-index: 10;
  105. }
  106.  
  107. .dark #sai-button-wrapper {
  108. background: #eeeeee4a;
  109. }
  110.  
  111. #sai-cancel-msg {
  112. font-size: 8px;
  113. color: #fff;
  114. position: absolute;
  115. bottom: -7px;
  116. right: 12px;
  117. transition: 0.2s;
  118. user-select: none;
  119. visibility: visible;
  120. opacity: 1;
  121. }
  122.  
  123. #sai-speech-button path {
  124. fill: #fff;
  125. }
  126.  
  127. #sai-lang-selector-wrapper {
  128. display: flex;
  129. align-items: center;
  130. }
  131.  
  132. #sai-no-voices {
  133. font-size: 12px;
  134. cursor: pointer;
  135. min-width: 75px;
  136. text-decoration: underline;
  137. color: #1abc9c;
  138. }
  139.  
  140. #sai-no-voices:hover {
  141. opacity: 0.5;
  142. }
  143.  
  144. #sai-lang-selector {
  145. font-size: 12px;
  146. height: 25px;
  147. padding: 0 10px;
  148. user-select: none;
  149. height: 30px;
  150. }
  151.  
  152. #sai-lang-selector.sai-hide {
  153. display: none;
  154. }
  155.  
  156. .dark #sai-lang-selector {
  157. color: #000 !important;
  158. }
  159.  
  160. #sai-settings-button {
  161. background-color: #1a82bc;
  162. padding: 3px 4px;
  163. border-radius: 5px;
  164. }
  165.  
  166. #sai-settings-button svg {
  167. width: 24px;
  168. height: 22px;
  169. margin-top: 1px;
  170. }
  171.  
  172. #sai-skip-read-aloud.sai-active:hover,
  173. #sai-disable-read-aloud:hover,
  174. #sai-settings-button:hover {
  175. opacity: 0.8;
  176. cursor: pointer;
  177. }
  178.  
  179.  
  180. #sai-disable-read-aloud {
  181. background-color: #1abc9c;
  182. padding: 3px 4px;
  183. border-radius: 5px;
  184. margin-left: 10px;
  185. margin-right: 10px;
  186. position: relative;
  187. }
  188.  
  189. #sai-disable-read-aloud.disabled {
  190. background-color: #cb4b4b;
  191. }
  192.  
  193. #sai-disable-read-aloud.disabled:before {
  194. content: "";
  195. width: 2px;
  196. height: 25px;
  197. background-color: #fff;
  198. position: absolute;
  199. transform: rotate(45deg);
  200. left: 13px;
  201. }
  202.  
  203. #sai-disable-read-aloud svg {
  204. fill: rgba(0,0,0,0.0);
  205. width: 24px;
  206. }
  207.  
  208. #sai-skip-read-aloud {
  209. background-color: #969696;
  210. padding: 3px 4px;
  211. border-radius: 5px;
  212. margin-left: 10px;
  213. position: relative;
  214. }
  215.  
  216. #sai-skip-read-aloud.sai-active {
  217. animation-name: yellow-pulsating-color;
  218. animation-duration: 2s;
  219. animation-iteration-count: infinite;
  220. background-color: #daa266;
  221. }
  222.  
  223. #sai-skip-read-aloud svg {
  224. fill: #fff;
  225. height: 16px;
  226. width: 24px;
  227. margin-top: 6px;
  228. }
  229.  
  230. @media only screen and (max-width:450px) {
  231. #sai-skip-read-aloud {
  232. display: none;
  233. }
  234.  
  235. .sai-compact #sai-skip-read-aloud {
  236. display: block;
  237. }
  238. }
  239.  
  240. @keyframes red-pulsating-color {
  241. 0% {
  242. background-color: #e02d2d;
  243. }
  244. 50% {
  245. background-color: #ef8585;
  246. }
  247. 100 {
  248. background-color: #e02d2d;
  249. }
  250. }
  251.  
  252. @keyframes yellow-pulsating-color {
  253. 0% {
  254. background-color: #daa266;
  255. }
  256. 50% {
  257. background-color: #c78d4f;
  258. }
  259. 100 {
  260. background-color: #daa266;
  261. }
  262. }
  263.  
  264. div.px-3.pt-2.pb-3.text-center.text-xs {
  265. padding: 6px;
  266. font-size: 0.6rem;
  267. }
  268.  
  269. #sai-error-message {
  270. position: fixed;
  271. top: 0;
  272. right: 0;
  273. width: 200px;
  274. min-height: 100px;
  275. background-color: #cb4b4b;
  276. padding: 15px;
  277. box-shadow: rgb(0 0 0 / 21%) 0px 0px 10px 2px;
  278. color: #fff;
  279. font-weight: bold;
  280. font-size: 12px;
  281. }
  282.  
  283.  
  284. /* ==== SETTINGS ====== */
  285.  
  286. #sai-settings-view {
  287. position: fixed;
  288. right: 0;
  289. top: 0;
  290. width: 100%;
  291. background-color: rgb(30 30 30 / 90%);
  292. height: 100vh;
  293. padding: 25px;
  294. z-index: 100000;
  295. }
  296.  
  297. #sai-settings-view.sai-hide {
  298. display: none;
  299. }
  300.  
  301. #sai-settings-view-inner {
  302. max-width: 700px;
  303. margin: 0 auto;
  304. display: flex;
  305. justify-content: space-between;
  306. }
  307.  
  308. .sai-settings-col {
  309. width: 45%;
  310. }
  311.  
  312. #sai-settings-header {
  313. display: flex;
  314. justify-content: space-between;
  315. align-items: flex-start;
  316. max-width: 700px;
  317. margin: 0 auto;
  318. border-bottom: 1px solid #777;
  319. margin-bottom: 25px;
  320. padding-bottom: 10px;
  321. }
  322.  
  323. #sai-settings-view a {
  324. color: #1abc9c;
  325. text-decoration: none;
  326. font-weight: bold;
  327. }
  328.  
  329. .sai-button {
  330. all: unset;
  331. background-color: #1abc9c;
  332. color: #fff;
  333. padding: 10px 15px;
  334. font-weight: bold;
  335. border-radius: 5px;
  336. font-size: 14px;
  337. color: #fff !important;
  338. cursor: pointer;
  339. line-height: 1.6;
  340. }
  341.  
  342. .sai-button:hover {
  343. opacity: 0.8;
  344. }
  345.  
  346.  
  347. #sai-settings-view h3,
  348. #sai-settings-view h4,
  349. #sai-settings-view p {
  350. color: #fff;
  351. margin-bottom: 25px;
  352. }
  353.  
  354. #sai-settings-view li {
  355. color: #fff;
  356. }
  357.  
  358. #sai-settings-view h3 {
  359. font-size: 20px;
  360. }
  361.  
  362. #sai-settings-view h4 {
  363. font-size: 17px;
  364. font-weight:bold;
  365. margin-bottom: 15px;
  366. }
  367.  
  368.  
  369. .sai-settings-section {
  370. margin-top: 35px;
  371. padding-top: 25px;
  372. border-top: 1px solid #777;
  373. }
  374.  
  375. #sai-settings-view li strong {
  376. color: #ffca92;
  377. }
  378.  
  379. #sai-settings-view ul {
  380. padding-left: 0;
  381. margin: 0;
  382. list-style: none;
  383. }
  384.  
  385. #sai-settings-view li {
  386. margin-top: 10px;
  387. }
  388.  
  389. #sai-settings-read-aloud-header {
  390.  
  391. }
  392.  
  393. #sai-settings-voice-link {
  394. display: inline-block;
  395. margin-top: 7px;
  396. font-size: 12px;
  397. }
  398.  
  399. .sai-slidecontainer {
  400. width: 100%;
  401. }
  402.  
  403. .sai-slider {
  404. -webkit-appearance: none;
  405. width: 100%;
  406. height: 15px;
  407. border-radius: 5px;
  408. background: #d3d3d3;
  409. outline: none;
  410. opacity: 0.7;
  411. -webkit-transition: 0.2s;
  412. transition: opacity 0.2s;
  413. }
  414.  
  415. .sai-slider:hover {
  416. opacity: 1;
  417. }
  418.  
  419. .sai-slider::-webkit-slider-thumb {
  420. -webkit-appearance: none;
  421. appearance: none;
  422. width: 25px;
  423. height: 25px;
  424. border-radius: 50%;
  425. background: #1abc9c;
  426. cursor: pointer;
  427. }
  428.  
  429. .sai-link-talkio {
  430. color: #ac99ff !important;
  431. }
  432.  
  433. @media only screen and (max-height: 720px) {
  434. #sai-settings-header {
  435. margin-bottom: 15px;
  436. padding-bottom: 0;
  437. }
  438.  
  439. #sai-settings-view {
  440. font-size: 12px;
  441. overflow-y: auto;
  442. }
  443.  
  444. #sai-settings-view h4 {
  445. font-size: 16px;
  446. }
  447.  
  448. .sai-settings-section {
  449. margin-top: 20px;
  450. padding-top: 10px;
  451. }
  452. }
  453.  
  454. /* ======== REPEAT BUTTON ======= */
  455. .sai-repeat-button {
  456. border-radius: 5px;
  457. width: 22px;
  458. height: 22px;
  459. cursor: pointer;
  460. position: relative;
  461. }
  462.  
  463. .sai-repeat-button.sai-disabled {
  464. display: none;
  465. }
  466.  
  467. .sai-repeat-button svg {
  468. width: 18px;
  469. height: 18px;
  470. margin-top: 2px;
  471. margin-left: 2px;
  472. }
  473.  
  474. .sai-repeat-button path {
  475. fill: #acacbe !important;
  476. }
  477.  
  478. .sai-repeat-button:hover {
  479. background: #ececf1;
  480. }
  481.  
  482. .sai-repeat-button:hover path {
  483. fill: #40414f !important;
  484. }
  485.  
  486. .dark .sai-repeat-button:hover {
  487. background: #40414f;
  488. }
  489.  
  490. .dark .sai-repeat-button:hover path {
  491. fill: #fff !important;
  492. }
  493.  
  494.  
  495. /* ======== HIDE SAI ======= */
  496. .sai-hidden #sai-input-wrapper,
  497. .sai-hidden #sai-lang-selector-wrapper,
  498. .sai-hidden #sai-skip-read-aloud,
  499. .sai-hidden #sai-disable-read-aloud {
  500. display: none;
  501. }
  502.  
  503. .sai-hidden #sai-button-wrapper {
  504. background: transparent;
  505. padding: 0;
  506. }
  507.  
  508. .sai-hidden #sai-settings-button {
  509. border-radius: 5px;
  510. position: fixed;
  511. top: 7px;
  512. right: 45px;
  513. z-index: 10000;
  514. }
  515.  
  516. @media only screen and (min-width: 768px) {
  517. .sai-hidden #sai-settings-button {
  518. top: 20px;
  519. right: 20px
  520. }
  521. }
  522.  
  523. @media only screen and (max-width: 768px) {
  524. form > div.relative.flex.h-full {
  525. flex-direction: column;
  526. }
  527.  
  528. #sai-input-wrapper {
  529. height: 50px;
  530. }
  531. }
  532.  
  533. /* ======== SAI COMPACT ======= */
  534. .sai-compact #sai-root {
  535. height: 0;
  536. margin: 0;
  537. position: relative;
  538. }
  539.  
  540. .sai-compact #sai-input-wrapper{
  541. position: absolute;
  542. width: 30px;
  543. height: 30px;
  544. right: 10px;
  545. top: 8px;
  546. border: none;
  547. z-index: 10;
  548. }
  549.  
  550. .sai-compact #sai-input-wrapper.is-idle {
  551. background: none;
  552. border: none;
  553. box-shadow: none;
  554. opacity: 0.5;
  555. }
  556.  
  557. .sai-compact .sai-input {
  558. display: none !important;
  559. }
  560.  
  561. .sai-compact #sai-speech-button {
  562. width: 20px !important;
  563. height: 20px !important;
  564. top: 4px !important;
  565. right: 0 !important;
  566. margin-right: 4px !important;
  567. }
  568.  
  569. .sai-compact #sai-speech-button svg {
  570. width: 20px !important;
  571. height: 20px !important;
  572. }
  573.  
  574. .sai-compact #sai-input-wrapper.is-idle #sai-speech-button svg path {
  575. fill: #999;
  576. }
  577.  
  578. .sai-compact #sai-cancel-msg {
  579. display: none;
  580. }
  581.  
  582. .sai-compact #sai-button-wrapper {
  583. position: absolute;
  584. bottom: 15px;
  585. right: 0;
  586. padding: 5px 7px;
  587. }
  588.  
  589. .sai-compact #sai-lang-selector {
  590. font-size: 10px !important;
  591. height: 25px;
  592. }
  593.  
  594. .sai-compact #sai-settings-button svg,
  595. .sai-compact #sai-disable-read-aloud svg{
  596. width: 20px !important;
  597. height: 20px !important;
  598. margin-top: 0px !important;
  599. }
  600.  
  601. .sai-compact #sai-skip-read-aloud svg {
  602. width: 20px !important;
  603. height: 13px !important;
  604. margin-top: 5px !important;
  605. }
  606.  
  607. .sai-compact #sai-disable-read-aloud.disabled:before {
  608. left: 11px;
  609. bottom: 1px;
  610. }
  611.  
  612. .sai-compact textarea {
  613. padding-right: 4rem !important;
  614. }
  615.  
  616. .sai-compact textarea + button {
  617. margin-right: 35px;
  618. }
  619.  
  620. @media only screen and (max-width: 900px) {
  621. .sai-compact .flex.ml-1.gap-0.justify-center{
  622. position: static;
  623. justify-content: flex-start !important;
  624. }
  625. }
  626.  
  627. @media only screen and (max-width: 768px) {
  628. .sai-compact .w-full.h-32.flex-shrink-0 {
  629. margin-top: 25px;
  630. }
  631.  
  632. .sai-compact .flex.ml-1.gap-0.justify-center{
  633. position: absolute;
  634. bottom: 62px;
  635. height: 30px;
  636. }
  637. }
  638.  
  639. @media only screen and (min-width: 768px) {
  640. .sai-compact #sai-input-wrapper {
  641. top: 12px;
  642. }
  643.  
  644. .sai-compact #sai-button-wrapper {
  645. bottom: 10px;
  646. }
  647.  
  648. .sai-compact .flex.ml-1.gap-0.justify-center{
  649. position: absolute;
  650. top: -46px;
  651. max-height: 36px;
  652. }
  653. }
  654. `
  655. document.body.appendChild(style);
  656. var logLevel;
  657. (function(e) {
  658. e.info = "info",
  659. e.warning = "warning",
  660. e.error = "error",
  661. e.verbose = "verbose",
  662. e.success = "success"
  663. }(logLevel || (logLevel = {})));
  664. class SAILogger {
  665. constructor(logToConsole=!0) {
  666. this.logToConsole = logToConsole,
  667. window.addEventListener("sai-print-logs", (()=>{
  668. console.log("All logs:"),
  669. console.log(t.allLogs)
  670. }
  671. ))
  672. }
  673. static info(msg, data) {
  674. this.instance.write(msg, logLevel.info, data)
  675. }
  676. static success(msg, data) {
  677. this.instance.write(msg, logLevel.success, data)
  678. }
  679. static warn(msg, data) {
  680. this.instance.write(msg, logLevel.warning, data)
  681. }
  682. static error(msg, err) {
  683. this.instance.write(msg, logLevel.error, err)
  684. }
  685. static verbose(msg, data) {
  686. this.instance.logToConsole && SAILogger.allLogs.push([Date.now(), logLevel.verbose, msg, data])
  687. }
  688. static setup() {
  689. if (!SAILogger.instance) {
  690. const logToConsole = "true" === window.localStorage.getItem("sai-log");
  691. this.instance = new SAILogger(logToConsole)
  692. }
  693. return SAILogger.instance
  694. }
  695. write(msg, level, data) {
  696. if (this.logToConsole) {
  697. const style = `color: ${this.getConsoleColor(level)}`;
  698. data ? console.log(`%c[${level}] ${msg}`, style, data) : console.log(`%c[${level}] ${msg}`, style),
  699. SAILogger.allLogs.push([Date.now(), level, msg, data])
  700. }
  701. }
  702. getConsoleColor(level) {
  703. return level === logLevel.info ? "#2e99d9" : level === logLevel.warning ? "#ffbb00" : level === logLevel.success ? "#1abc9c" : "#b91e1e"
  704. }
  705. }
  706. SAILogger.allLogs = [];
  707. class ErrorMessage {
  708. constructor(element) {
  709. this.element = element,
  710. this.isVisible = !1
  711. }
  712. write(html, delay=3e3) {
  713. this.isVisible && clearTimeout(this.timer),
  714. this.element.innerHTML = html,
  715. this.setVisible(!0),
  716. this.timer = setTimeout((()=>{
  717. this.setVisible(!1),
  718. this.element.innerHTML = ""
  719. }
  720. ), delay)
  721. }
  722. setVisible(v) {
  723. this.element.style.display = v ? "block" : "none",
  724. this.isVisible = v
  725. }
  726. }
  727. function matchLanguageCode(code, name) {
  728. return code === name || ("zh-CN" === code && "cmn-Hans-CN" === name || ("zh-TW" === code && "cmn-Hant-TW" === name || "zh-HK" === code && "yue-Hant-HK" === name))
  729. }
  730. const staticLangSupportList = [
  731. ["English (US)", "en-US"],
  732. ["English (UK)", "en-GB"],
  733. ["English (AU)", "en-AU"],
  734. ["English (CA)", "en-CA"],
  735. ["English (IN)", "en-IN"],
  736. ["English (NZ)", "en-NZ"],
  737. ["普通话 (中国大陆)", "cmn-Hans-CN"],
  738. ["中文 (台灣)", "cmn-Hant-TW"],
  739. ["粵語 (香港)", "yue-Hant-HK"],
  740. ["Afrikaans", "af-ZA"],
  741. ["Bahasa Indonesia", "id-ID"],
  742. ["Bahasa Melayu", "ms-MY"],
  743. ["Català", "ca-ES"],
  744. ["Čeština", "cs-CZ"],
  745. ["Dansk", "da-DK"],
  746. ["Deutsch", "de-DE"],
  747. ["Español (ES)", "es-ES"],
  748. ["Español (MX)", "es-MX"],
  749. ["Español (AR)", "es-AR"],
  750. ["Español (CO)", "es-CO"],
  751. ["Español (PE)", "es-PE"],
  752. ["Español (VE)", "es-VE"],
  753. ["Euskara", "eu-ES"],
  754. ["Français", "fr-FR"],
  755. ["Galego", "gl-ES"],
  756. ["Hrvatski", "hr_HR"],
  757. ["IsiZulu", "zu-ZA"],
  758. ["Íslenska", "is-IS"],
  759. ["Italiano", "it-IT"],
  760. ["Magyar", "hu-HU"],
  761. ["Nederlands", "nl-NL"],
  762. ["Norsk bokmål", "nb-NO"],
  763. ["Polski", "pl-PL"],
  764. ["Português (PT)", "pt-PT"],
  765. ["Português (BR)", "pt-BR"],
  766. ["Română", "ro-RO"],
  767. ["Slovenčina", "sk-SK"],
  768. ["Suomi", "fi-FI"],
  769. ["Svenska", "sv-SE"],
  770. ["Türkçe", "tr-TR"],
  771. ["български", "bg-BG"],
  772. ["日本語", "ja-JP"],
  773. ["한국어", "ko-KR"],
  774. ["Pусский", "ru-RU"],
  775. ["Српски", "sr-RS"]
  776. ];
  777. let allVoices = [];
  778. async function getVoiceSupportList() {
  779. if (allVoices.length > 0)
  780. return allVoices;
  781. const voices = await new Promise((resolve=>{
  782. window.speechSynthesis.onvoiceschanged = ()=>{
  783. const voices = window.speechSynthesis.getVoices();
  784. resolve(voices)
  785. }
  786. }
  787. ));
  788. return staticLangSupportList.forEach((langSupport=>{
  789. voices.some((v=>matchLanguageCode(v.lang, langSupport[1]))) ? allVoices.push(langSupport) : SAILogger.warn(`${langSupport[0]} not supported. Removed from selector.`)
  790. }
  791. )),
  792. allVoices
  793. }
  794. class LanguageSelector {
  795. constructor(selectionCb, code) {
  796. this.selectionCb = selectionCb,
  797. this.selected = code,
  798. this.storageKey = "sai-language",
  799. this.setDefaultFromStorage(),
  800. this.element = document.createElement("div"),
  801. this.selector = document.createElement("select"),
  802. this.element.id = "sai-lang-selector-wrapper",
  803. this.selector.id = "sai-lang-selector",
  804. getVoiceSupportList().then((list=>{
  805. if (0 === list.length) {
  806. this.selector.classList.add("sai-hide");
  807. const d = document.createElement("div");
  808. return d.id = "sai-no-voices",
  809. d.innerHTML = "<a href='https://voicecontrol.chat/install-voices' target='_blank'>Install voices</a>",
  810. void this.element.appendChild(d)
  811. }
  812. list.forEach((([name,code])=>{
  813. const t = document.createElement("option");
  814. t.innerText = name,
  815. t.value = code,
  816. code === this.selected && (t.selected = !0),
  817. this.selector.appendChild(t)
  818. }
  819. )),
  820. this.element.appendChild(this.selector),
  821. this.selector.onchange = e=>{
  822. const n = e.target;
  823. this.selectLanguage(n.value)
  824. }
  825. }
  826. ))
  827. }
  828. selectLanguage(code) {
  829. window.localStorage.setItem(this.storageKey, code),
  830. this.selectionCb(code)
  831. }
  832. setDefaultFromStorage() {
  833. let code = window.localStorage.getItem(this.storageKey);
  834. code && (this.selected = code,
  835. this.selectLanguage(code))
  836. }
  837. }
  838. class ReadAloudImpl {
  839. constructor(lang, reset, waitForContent) {
  840. this.lang = lang,
  841. this.waitForContent = waitForContent,
  842. this.lastText = "",
  843. this.lastRead = Date.now(),
  844. this.lastUtter = Date.now(),
  845. this.lastUtterCharCount = 0,
  846. this.lastTimeout = 0,
  847. this.lastTimeSinceLastUtter = 0,
  848. this.synth = window.speechSynthesis,
  849. this.queue = [],
  850. this.enabled = !0,
  851. this.storageKey = "sai-read-aloud",
  852. this.queueIdle = !0,
  853. this.disableButton = document.createElement("div"),
  854. this.disableButton.innerHTML = '<?xml version="1.0" encoding="iso-8859-1"?>\n\x3c!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --\x3e\n<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"\n\t viewBox="0 0 496.159 496.159" style="enable-background:new 0 0 496.159 496.159;" xml:space="preserve">\n<path class="sai-svg-color-path" d="M496.159,248.085c0-137.023-111.07-248.082-248.076-248.082C111.071,0.003,0,111.063,0,248.085\n\tc0,137.001,111.07,248.07,248.083,248.07C385.089,496.155,496.159,385.086,496.159,248.085z"/>\n<g>\n\t<path style="fill:#FFFFFF;" d="M247.711,125.252c-3.41-1.851-7.559-1.688-10.813,0.426l-95.137,61.789h-35.164\n\t\tc-5.845,0-10.583,4.738-10.583,10.584v92.727c0,5.845,4.738,10.583,10.583,10.583h35.164l95.137,61.79\n\t\tc1.748,1.135,3.753,1.707,5.765,1.707c1.733,0,3.471-0.425,5.049-1.281c3.41-1.852,5.534-5.421,5.534-9.301V134.553\n\t\tC253.244,130.672,251.121,127.103,247.711,125.252z"/>\n\t<path style="fill:#FFFFFF;" d="M282.701,319.271c0.894,0,1.801-0.162,2.685-0.504c24.239-9.412,40.524-38.49,40.524-72.359\n\t\tc0-29.957-13.2-57.049-33.63-69.018c-3.534-2.072-8.08-0.885-10.153,2.65c-2.073,3.536-0.885,8.082,2.651,10.153\n\t\tc15.971,9.358,26.291,31.424,26.291,56.214c0,27.359-12.77,51.424-31.055,58.525c-3.82,1.481-5.714,5.781-4.231,9.602\n\t\tC276.924,317.474,279.729,319.271,282.701,319.271z"/>\n\t<path style="fill:#FFFFFF;" d="M302.073,350.217c0.895,0,1.802-0.162,2.684-0.504c34.046-13.219,57.822-55.979,57.822-103.988\n\t\tc0-43.187-18.884-82.156-48.11-99.279c-3.534-2.072-8.082-0.885-10.152,2.652c-2.073,3.535-0.885,8.081,2.651,10.152\n\t\tc24.768,14.512,40.771,48.455,40.771,86.475c0,42.027-19.883,79.1-48.353,90.154c-3.82,1.481-5.715,5.781-4.231,9.602\n\t\tC296.295,348.418,299.1,350.217,302.073,350.217z"/>\n\t<path style="fill:#FFFFFF;" d="M322.025,379.715c-3.005,0-5.841-1.818-6.994-4.788c-1.499-3.861,0.416-8.206,4.277-9.706\n\t\tc38.764-15.051,65.837-64.404,65.837-120.019c0-50.136-21.609-95.192-55.052-114.786c-3.574-2.094-4.773-6.688-2.68-10.262\n\t\tc2.094-3.574,6.688-4.774,10.263-2.68c37.948,22.232,62.469,72.369,62.469,127.728c0,61.66-31.009,116.764-75.409,134.002\n\t\tC323.846,379.551,322.928,379.715,322.025,379.715z"/>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n<g>\n</g>\n</svg>\n',
  855. this.disableButton.id = "sai-disable-read-aloud",
  856. this.disableButton.title = "Toggle read aloud",
  857. this.skipButton = document.createElement("div"),
  858. this.skipButton.innerHTML = '<?xml version="1.0" encoding="UTF-8"?>\n<svg width="700pt" height="700pt" version="1.1" viewBox="0 0 700 700" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\n <defs>\n <symbol id="s" overflow="visible">\n <path d="m18.766-1.125c-0.96875 0.5-1.9805 0.875-3.0312 1.125-1.043 0.25781-2.1367 0.39062-3.2812 0.39062-3.3984 0-6.0898-0.94531-8.0781-2.8438-1.9922-1.9062-2.9844-4.4844-2.9844-7.7344 0-3.2578 0.99219-5.8359 2.9844-7.7344 1.9883-1.9062 4.6797-2.8594 8.0781-2.8594 1.1445 0 2.2383 0.13281 3.2812 0.39062 1.0508 0.25 2.0625 0.625 3.0312 1.125v4.2188c-0.98047-0.65625-1.9453-1.1406-2.8906-1.4531-0.94922-0.3125-1.9492-0.46875-3-0.46875-1.875 0-3.3516 0.60547-4.4219 1.8125-1.0742 1.1992-1.6094 2.8555-1.6094 4.9688 0 2.1055 0.53516 3.7617 1.6094 4.9688 1.0703 1.1992 2.5469 1.7969 4.4219 1.7969 1.0508 0 2.0508-0.14844 3-0.45312 0.94531-0.3125 1.9102-0.80078 2.8906-1.4688z"/>\n </symbol>\n <symbol id="b" overflow="visible">\n <path d="m13.734-11.141c-0.4375-0.19531-0.87109-0.34375-1.2969-0.4375-0.41797-0.10156-0.83984-0.15625-1.2656-0.15625-1.2617 0-2.2305 0.40625-2.9062 1.2188-0.67969 0.80469-1.0156 1.9531-1.0156 3.4531v7.0625h-4.8906v-15.312h4.8906v2.5156c0.625-1 1.3438-1.7266 2.1562-2.1875 0.82031-0.46875 1.8008-0.70312 2.9375-0.70312 0.16406 0 0.34375 0.011719 0.53125 0.03125 0.19531 0.011719 0.47656 0.039062 0.84375 0.078125z"/>\n </symbol>\n <symbol id="a" overflow="visible">\n <path d="m17.641-7.7031v1.4062h-11.453c0.125 1.1484 0.53906 2.0078 1.25 2.5781 0.70703 0.57422 1.7031 0.85938 2.9844 0.85938 1.0312 0 2.082-0.14844 3.1562-0.45312 1.082-0.3125 2.1914-0.77344 3.3281-1.3906v3.7656c-1.1562 0.4375-2.3125 0.76562-3.4688 0.98438-1.1562 0.22656-2.3125 0.34375-3.4688 0.34375-2.7734 0-4.9297-0.70312-6.4688-2.1094-1.5312-1.4062-2.2969-3.3789-2.2969-5.9219 0-2.5 0.75391-4.4609 2.2656-5.8906 1.5078-1.4375 3.582-2.1562 6.2188-2.1562 2.4062 0 4.332 0.73047 5.7812 2.1875 1.4453 1.4492 2.1719 3.3828 2.1719 5.7969zm-5.0312-1.625c0-0.92578-0.27344-1.6719-0.8125-2.2344-0.54297-0.57031-1.25-0.85938-2.125-0.85938-0.94922 0-1.7188 0.26562-2.3125 0.79688s-0.96484 1.2969-1.1094 2.2969z"/>\n </symbol>\n <symbol id="d" overflow="visible">\n <path d="m9.2188-6.8906c-1.0234 0-1.793 0.17188-2.3125 0.51562-0.51172 0.34375-0.76562 0.85547-0.76562 1.5312 0 0.625 0.20703 1.1172 0.625 1.4688 0.41406 0.34375 0.98828 0.51562 1.7188 0.51562 0.92578 0 1.7031-0.32812 2.3281-0.98438 0.63281-0.66406 0.95312-1.4922 0.95312-2.4844v-0.5625zm7.4688-1.8438v8.7344h-4.9219v-2.2656c-0.65625 0.92969-1.3984 1.6055-2.2188 2.0312-0.82422 0.41406-1.8242 0.625-3 0.625-1.5859 0-2.8711-0.45703-3.8594-1.375-0.99219-0.92578-1.4844-2.1289-1.4844-3.6094 0-1.7891 0.61328-3.1016 1.8438-3.9375 1.2383-0.84375 3.1797-1.2656 5.8281-1.2656h2.8906v-0.39062c0-0.76953-0.30859-1.332-0.92188-1.6875-0.61719-0.36328-1.5703-0.54688-2.8594-0.54688-1.0547 0-2.0312 0.10547-2.9375 0.3125-0.89844 0.21094-1.7305 0.52344-2.5 0.9375v-3.7344c1.0391-0.25 2.0859-0.44141 3.1406-0.57812 1.0625-0.13281 2.125-0.20312 3.1875-0.20312 2.7578 0 4.75 0.54688 5.9688 1.6406 1.2266 1.0859 1.8438 2.8555 1.8438 5.3125z"/>\n </symbol>\n <symbol id="c" overflow="visible">\n <path d="m7.7031-19.656v4.3438h5.0469v3.5h-5.0469v6.5c0 0.71094 0.14062 1.1875 0.42188 1.4375s0.83594 0.375 1.6719 0.375h2.5156v3.5h-4.1875c-1.9375 0-3.3125-0.39844-4.125-1.2031-0.80469-0.8125-1.2031-2.1797-1.2031-4.1094v-6.5h-2.4219v-3.5h2.4219v-4.3438z"/>\n </symbol>\n <symbol id="j" overflow="visible">\n <path d="m12.766-13.078v-8.2031h4.9219v21.281h-4.9219v-2.2188c-0.66797 0.90625-1.4062 1.5703-2.2188 1.9844s-1.7578 0.625-2.8281 0.625c-1.8867 0-3.4336-0.75-4.6406-2.25-1.2109-1.5-1.8125-3.4258-1.8125-5.7812 0-2.3633 0.60156-4.2969 1.8125-5.7969 1.207-1.5 2.7539-2.25 4.6406-2.25 1.0625 0 2 0.21484 2.8125 0.64062 0.82031 0.42969 1.5664 1.0859 2.2344 1.9688zm-3.2188 9.9219c1.0391 0 1.8359-0.37891 2.3906-1.1406 0.55078-0.76953 0.82812-1.8828 0.82812-3.3438 0-1.457-0.27734-2.5664-0.82812-3.3281-0.55469-0.76953-1.3516-1.1562-2.3906-1.1562-1.043 0-1.8398 0.38672-2.3906 1.1562-0.55469 0.76172-0.82812 1.8711-0.82812 3.3281 0 1.4609 0.27344 2.5742 0.82812 3.3438 0.55078 0.76172 1.3477 1.1406 2.3906 1.1406z"/>\n </symbol>\n <symbol id="i" overflow="visible">\n <path d="m10.5-3.1562c1.0508 0 1.8516-0.37891 2.4062-1.1406 0.55078-0.76953 0.82812-1.8828 0.82812-3.3438 0-1.457-0.27734-2.5664-0.82812-3.3281-0.55469-0.76953-1.3555-1.1562-2.4062-1.1562-1.0547 0-1.8594 0.38672-2.4219 1.1562-0.55469 0.77344-0.82812 1.8828-0.82812 3.3281 0 1.4492 0.27344 2.5586 0.82812 3.3281 0.5625 0.77344 1.3672 1.1562 2.4219 1.1562zm-3.25-9.9219c0.67578-0.88281 1.4219-1.5391 2.2344-1.9688 0.82031-0.42578 1.7656-0.64062 2.8281-0.64062 1.8945 0 3.4453 0.75 4.6562 2.25 1.207 1.5 1.8125 3.4336 1.8125 5.7969 0 2.3555-0.60547 4.2812-1.8125 5.7812-1.2109 1.5-2.7617 2.25-4.6562 2.25-1.0625 0-2.0078-0.21094-2.8281-0.625-0.8125-0.42578-1.5586-1.0859-2.2344-1.9844v2.2188h-4.8906v-21.281h4.8906z"/>\n </symbol>\n <symbol id="h" overflow="visible">\n <path d="m0.34375-15.312h4.8906l4.125 10.391 3.5-10.391h4.8906l-6.4375 16.766c-0.64844 1.6953-1.4023 2.8828-2.2656 3.5625-0.86719 0.6875-2 1.0312-3.4062 1.0312h-2.8438v-3.2188h1.5312c0.83203 0 1.4375-0.13672 1.8125-0.40625 0.38281-0.26172 0.67969-0.73047 0.89062-1.4062l0.14062-0.42188z"/>\n </symbol>\n <symbol id="g" overflow="visible">\n <path d="m2.3594-21.281h4.8906v21.281h-4.8906z"/>\n </symbol>\n <symbol id="f" overflow="visible">\n <path d="m16.547-12.766c0.61328-0.94531 1.3477-1.6719 2.2031-2.1719 0.85156-0.5 1.7891-0.75 2.8125-0.75 1.7578 0 3.0977 0.54688 4.0156 1.6406 0.92578 1.0859 1.3906 2.6562 1.3906 4.7188v9.3281h-4.9219v-7.9844-0.35938c0.007813-0.13281 0.015625-0.32031 0.015625-0.5625 0-1.082-0.16406-1.8633-0.48438-2.3438-0.3125-0.48828-0.82422-0.73438-1.5312-0.73438-0.92969 0-1.6484 0.38672-2.1562 1.1562-0.51172 0.76172-0.77344 1.8672-0.78125 3.3125v7.5156h-4.9219v-7.9844c0-1.6953-0.14844-2.7852-0.4375-3.2656-0.29297-0.48828-0.8125-0.73438-1.5625-0.73438-0.9375 0-1.6641 0.38672-2.1719 1.1562-0.51172 0.76172-0.76562 1.8594-0.76562 3.2969v7.5312h-4.9219v-15.312h4.9219v2.2344c0.60156-0.86328 1.2891-1.5156 2.0625-1.9531 0.78125-0.4375 1.6406-0.65625 2.5781-0.65625 1.0625 0 2 0.25781 2.8125 0.76562 0.8125 0.51172 1.4258 1.2305 1.8438 2.1562z"/>\n </symbol>\n <symbol id="r" overflow="visible">\n <path d="m12.422-21.281v3.2188h-2.7031c-0.6875 0-1.1719 0.125-1.4531 0.375-0.27344 0.25-0.40625 0.6875-0.40625 1.3125v1.0625h4.1875v3.5h-4.1875v11.812h-4.8906v-11.812h-2.4375v-3.5h2.4375v-1.0625c0-1.6641 0.46094-2.8984 1.3906-3.7031 0.92578-0.80078 2.3672-1.2031 4.3281-1.2031z"/>\n </symbol>\n <symbol id="e" overflow="visible">\n <path d="m9.6406-12.188c-1.0859 0-1.9141 0.39062-2.4844 1.1719-0.57422 0.78125-0.85938 1.9062-0.85938 3.375s0.28516 2.5938 0.85938 3.375c0.57031 0.77344 1.3984 1.1562 2.4844 1.1562 1.0625 0 1.875-0.38281 2.4375-1.1562 0.57031-0.78125 0.85938-1.9062 0.85938-3.375s-0.28906-2.5938-0.85938-3.375c-0.5625-0.78125-1.375-1.1719-2.4375-1.1719zm0-3.5c2.6328 0 4.6914 0.71484 6.1719 2.1406 1.4766 1.418 2.2188 3.3867 2.2188 5.9062 0 2.5117-0.74219 4.4805-2.2188 5.9062-1.4805 1.418-3.5391 2.125-6.1719 2.125-2.6484 0-4.7148-0.70703-6.2031-2.125-1.4922-1.4258-2.2344-3.3945-2.2344-5.9062 0-2.5195 0.74219-4.4883 2.2344-5.9062 1.4883-1.4258 3.5547-2.1406 6.2031-2.1406z"/>\n </symbol>\n <symbol id="q" overflow="visible">\n <path d="m17.75-9.3281v9.3281h-4.9219v-7.1094c0-1.3438-0.03125-2.2656-0.09375-2.7656s-0.16797-0.86719-0.3125-1.1094c-0.1875-0.3125-0.44922-0.55469-0.78125-0.73438-0.32422-0.17578-0.69531-0.26562-1.1094-0.26562-1.0234 0-1.8242 0.39844-2.4062 1.1875-0.58594 0.78125-0.875 1.8711-0.875 3.2656v7.5312h-4.8906v-21.281h4.8906v8.2031c0.73828-0.88281 1.5195-1.5391 2.3438-1.9688 0.83203-0.42578 1.75-0.64062 2.75-0.64062 1.7695 0 3.1133 0.54688 4.0312 1.6406 0.91406 1.0859 1.375 2.6562 1.375 4.7188z"/>\n </symbol>\n <symbol id="p" overflow="visible">\n <path d="m2.5781-20.406h5.875l7.4219 14v-14h4.9844v20.406h-5.875l-7.4219-14v14h-4.9844z"/>\n </symbol>\n <symbol id="o" overflow="visible">\n <path d="m2.1875-5.9688v-9.3438h4.9219v1.5312c0 0.83594-0.007813 1.875-0.015625 3.125-0.011719 1.25-0.015625 2.0859-0.015625 2.5 0 1.2422 0.03125 2.1328 0.09375 2.6719 0.070313 0.54297 0.17969 0.93359 0.32812 1.1719 0.20703 0.32422 0.47266 0.57422 0.79688 0.75 0.32031 0.16797 0.69141 0.25 1.1094 0.25 1.0195 0 1.8203-0.39062 2.4062-1.1719 0.58203-0.78125 0.875-1.8672 0.875-3.2656v-7.5625h4.8906v15.312h-4.8906v-2.2188c-0.74219 0.89844-1.5234 1.5586-2.3438 1.9844-0.82422 0.41406-1.7344 0.625-2.7344 0.625-1.7617 0-3.1055-0.53906-4.0312-1.625-0.92969-1.082-1.3906-2.6602-1.3906-4.7344z"/>\n </symbol>\n <symbol id="n" overflow="visible">\n <path d="m17.75-9.3281v9.3281h-4.9219v-7.1406c0-1.3203-0.03125-2.2344-0.09375-2.7344s-0.16797-0.86719-0.3125-1.1094c-0.1875-0.3125-0.44922-0.55469-0.78125-0.73438-0.32422-0.17578-0.69531-0.26562-1.1094-0.26562-1.0234 0-1.8242 0.39844-2.4062 1.1875-0.58594 0.78125-0.875 1.8711-0.875 3.2656v7.5312h-4.8906v-15.312h4.8906v2.2344c0.73828-0.88281 1.5195-1.5391 2.3438-1.9688 0.83203-0.42578 1.75-0.64062 2.75-0.64062 1.7695 0 3.1133 0.54688 4.0312 1.6406 0.91406 1.0859 1.375 2.6562 1.375 4.7188z"/>\n </symbol>\n <symbol id="m" overflow="visible">\n <path d="m2.5781-20.406h8.7344c2.5938 0 4.582 0.57812 5.9688 1.7344 1.3945 1.1484 2.0938 2.7891 2.0938 4.9219 0 2.1367-0.69922 3.7812-2.0938 4.9375-1.3867 1.1562-3.375 1.7344-5.9688 1.7344h-3.4844v7.0781h-5.25zm5.25 3.8125v5.7031h2.9219c1.0195 0 1.8047-0.25 2.3594-0.75 0.5625-0.5 0.84375-1.2031 0.84375-2.1094 0-0.91406-0.28125-1.6172-0.84375-2.1094-0.55469-0.48828-1.3398-0.73438-2.3594-0.73438z"/>\n </symbol>\n <symbol id="l" overflow="visible">\n <path d="m2.3594-15.312h4.8906v15.031c0 2.0508-0.49609 3.6172-1.4844 4.7031-0.98047 1.082-2.4062 1.625-4.2812 1.625h-2.4219v-3.2188h0.85938c0.92578 0 1.5625-0.21094 1.9062-0.625 0.35156-0.41797 0.53125-1.2461 0.53125-2.4844zm0-5.9688h4.8906v4h-4.8906z"/>\n </symbol>\n <symbol id="k" overflow="visible">\n <path d="m14.719-14.828v3.9844c-0.65625-0.45703-1.3242-0.79688-2-1.0156-0.66797-0.21875-1.3594-0.32812-2.0781-0.32812-1.3672 0-2.4336 0.40234-3.2031 1.2031-0.76172 0.79297-1.1406 1.9062-1.1406 3.3438 0 1.4297 0.37891 2.543 1.1406 3.3438 0.76953 0.79297 1.8359 1.1875 3.2031 1.1875 0.75781 0 1.4844-0.10938 2.1719-0.32812 0.6875-0.22656 1.3203-0.56641 1.9062-1.0156v4c-0.76172 0.28125-1.5391 0.48828-2.3281 0.625-0.78125 0.14453-1.5742 0.21875-2.375 0.21875-2.7617 0-4.9219-0.70703-6.4844-2.125-1.5547-1.4141-2.3281-3.3828-2.3281-5.9062 0-2.5312 0.77344-4.5039 2.3281-5.9219 1.5625-1.4141 3.7227-2.125 6.4844-2.125 0.80078 0 1.5938 0.074219 2.375 0.21875 0.78125 0.13672 1.5547 0.35156 2.3281 0.64062z"/>\n </symbol>\n </defs>\n <g>\n <path d="m134.69 38.246 293.43 235.17c4.2656 3.2578 3.6094 9.9531 0 13.102l-293.43 235.11c-6.4414 4.7617-13.645 0.21875-13.645-6.5508v-74.953c0-5.0703 2.1523-6.9141 5.6211-9.8086l187.64-150.36-189.11-151.59c-3.1445-2.5273-4.1523-4.9883-4.1523-8.6016v-74.98c0-7.6094 7.7539-10.453 13.645-6.5508z" fill-rule="evenodd"/>\n <path d="m570.56 36.402c4.6367 0 8.3945 3.7578 8.3945 8.3945v470.29c0 4.6367-3.7578 8.3945-8.3945 8.3945h-65.09 0.003906c-4.6367 0-8.3945-3.7578-8.3984-8.3945v-470.29c0.003906-4.6367 3.7617-8.3945 8.3984-8.3945h65.09z" fill-rule="evenodd"/>\n </g>\n</svg>\n',
  859. this.skipButton.id = "sai-skip-read-aloud",
  860. this.skipButton.title = "Skip read aloud",
  861. window.speechSynthesis.cancel(),
  862. this.disableButton.addEventListener("click", (()=>{
  863. this.enabled ? this.disableReadAloud() : this.enableReadAloud()
  864. }
  865. )),
  866. this.skipButton.onclick = ()=>{
  867. this.skipReading()
  868. }
  869. ,
  870. this.setReadAloudFromStorage(),
  871. SAILogger.info(`reInit ${reset}, lastTextLength: ${this.lastText.length}`),
  872. reset && this.reset()
  873. }
  874. async runQueue() {
  875. if (SAILogger.info(`Queue is idle: ${this.queueIdle}`),
  876. this.queue.length > 0 && this.queueIdle) {
  877. this.skipButton.classList.add("sai-active"),
  878. this.queueIdle = !1;
  879. const text = this.queue.shift();
  880. await this.readAloud(text),
  881. this.queueIdle = !0,
  882. this.skipButton.classList.remove("sai-active"),
  883. this.queue.length > 0 && this.runQueue()
  884. }
  885. }
  886. observerCallback(callback) {
  887. const text = this.getText();
  888. if (0 === text.length)
  889. SAILogger.info("No text, reset"),
  890. this.reset();
  891. else if (this.waitForContent)
  892. return SAILogger.info("Wait for content"),
  893. this.lastText = text,
  894. void (this.waitForContent = !1);
  895. const leftText = text.replace(this.lastText.trim(), "").trim()
  896. , lastChar = leftText[leftText.length - 1]
  897. , longTimeSince = this.lastRead + 1e4 < Date.now();
  898. if (leftText.length > 0 && ("." === lastChar || "?" === lastChar || "!" === lastChar || ":" === lastChar || "。" === lastChar || lastChar)) {
  899. longTimeSince && (SAILogger.warn(`Long time since last read. Queue length: ${this.queue.length}`),
  900. this.queueIdle = !0),
  901. SAILogger.info(`Push to queue: ${leftText}`);
  902. leftText.split(".").filter((t=>t.length > 0)).forEach((t=>{
  903. this.queue.push(t)
  904. }
  905. )),
  906. this.runQueue(),
  907. this.lastRead = Date.now(),
  908. this.lastText = text
  909. }
  910. }
  911. setLang(langCode) {
  912. this.lang = langCode
  913. }
  914. skipReading() {
  915. this.synth.cancel(),
  916. this.queue = [],
  917. this.queueIdle = !0;
  918. const bases = document.querySelectorAll(".text-base");
  919. for (var i = 0; i < bases.length; i++)
  920. bases[i]?.classList.add("sai-skip")
  921. }
  922. repeat(markdown) {
  923. this.synth.cancel(),
  924. this.queue = [],
  925. this.queueIdle = !0;
  926. const text = this.getText(markdown);
  927. SAILogger.info(`Repeat: ${text}`),
  928. this.queue.push(text),
  929. this.runQueue()
  930. }
  931. readAloud(text) {
  932. return new Promise(((resolve,reject)=>{
  933. if (!this.enabled)
  934. return SAILogger.info("Read aloud disabled"),
  935. void resolve(void 0);
  936. if (!text)
  937. return SAILogger.info("No text to read"),
  938. void resolve(void 0);
  939. if (!document.getElementById("sai-root"))
  940. return void resolve(void 0);
  941. let formatText = text.replace(/([0-9]+)\.(?=[0-9]+(?!\.))/g, "$1,");
  942. this.synth = window.speechSynthesis;
  943. const utterThis = new SpeechSynthesisUtterance(formatText)
  944. , langVoices = this.synth.getVoices().reverse().filter((e=>s(e.lang, this.lang)))
  945. , preferenceVoice = window.localStorage.getItem("sai-voice-preference" + this.lang)
  946. , voice = langVoices.find((v=>v.voiceURI === preferenceVoice)) ?? langVoices[0];
  947. if (!voice)
  948. throw new Error(`unknown voice: ${voice} lang: ${this.lang}`);
  949. utterThis.volume = 1,
  950. utterThis.voice = voice;
  951. const speed = window.localStorage.getItem("sai-voice-speed-v2");
  952. speed || SAILogger.error("No speed stored in storage");
  953. // speedStringToRate
  954. const rate = function(speed) {
  955. switch (speed) {
  956. case "1":
  957. return .1;
  958. case "2":
  959. return .2;
  960. case "3":
  961. return .3;
  962. case "4":
  963. return .4;
  964. case "5":
  965. return .5;
  966. case "6":
  967. return .6;
  968. case "7":
  969. return .7;
  970. case "8":
  971. return .8;
  972. case "9":
  973. return .9;
  974. case "10":
  975. default:
  976. return 1;
  977. case "11":
  978. return 1.1;
  979. case "12":
  980. return 1.13;
  981. case "13":
  982. return 1.15;
  983. case "14":
  984. return 1.17;
  985. case "15":
  986. return 1.2;
  987. case "16":
  988. return 1.25;
  989. case "17":
  990. return 1.3;
  991. case "18":
  992. return 1.35;
  993. case "19":
  994. return 1.4;
  995. case "20":
  996. return 1.45
  997. }
  998. }(speed);
  999. let timer;
  1000. utterThis.rate = rate,
  1001. SAILogger.success(`Voice name: ${voice.name}, lang: ${voice.lang}, rate: ${speed}: ${formatText}`);
  1002. const read = ()=>{
  1003. const [timeout,timeSinceLastUtter] = function(lang, rate, lastUtter, lastTimeout, lastUtterCharCount, lastTimeSinceLastUtter) {
  1004. const timeSinceLastUtter = Date.now() - lastUtter
  1005. , timeout = function(lang, lastUtterCharCount, rate) {
  1006. let i = 100;
  1007. return "zh-CN" !== lang && "zh-TW" !== lang && "zh-HK" !== lang || (i = 240),
  1008. "zh-TW" === lang && (i = 300),
  1009. "ja-JP" === lang && (i = 260),
  1010. "ko-KR" === lang && (i = 240),
  1011. 7e3 + lastUtterCharCount * i * (1 / rate)
  1012. }(lang, lastUtterCharCount, rate);
  1013. if (SAILogger.warn(`[resumeInfinity] Time since last utter: ${timeSinceLastUtter.toFixed(1)}. Timeout: ${timeout.toFixed(1)}. Last char count: ${lastUtterCharCount}`),
  1014. window.navigator.userAgent.search("Mac") > -1 && 0 === r && o > 0) {
  1015. const diff = lastTimeout - lastTimeSinceLastUtter
  1016. , n = diff / lastTimeout * 100;
  1017. SAILogger.warn(`Last timeout safety gap: ${diff.toFixed(1)}ms. ${n.toFixed(1)}%`),
  1018. n < 25 && SAILogger.error(`________Safety gap ${n.toFixed(1)}% too low!________`)
  1019. }
  1020. return timeSinceLastUtter > timeout ? (SAILogger.error(`No utter timeout ${timeout.toFixed(1)} - cancel.`),
  1021. window.speechSynthesis.cancel(),
  1022. setTimeout((()=>{
  1023. window.speechSynthesis.resume()
  1024. }
  1025. ), 50),
  1026. [0, 0]) : [timeout, timeSinceLastUtter]
  1027. }(voice.lang, rate, this.lastUtter, this.lastTimeout, this.lastUtterCharCount, this.lastTimeSinceLastUtter);
  1028. this.lastTimeout = timeout,
  1029. this.lastTimeSinceLastUtter = timeSinceLastUtter,
  1030. window.speechSynthesis.pause(),
  1031. window.speechSynthesis.resume(),
  1032. timer = setTimeout(read, 7e3)
  1033. }
  1034. ;
  1035. utterThis.addEventListener("error", (e=>{
  1036. SAILogger.error(`Read aloud error ${e.error}`, e),
  1037. resolve(void 0),
  1038. clearTimeout(h)
  1039. }
  1040. )),
  1041. utterThis.addEventListener("start", (()=>{
  1042. SAILogger.info(`Speech has started. Volume: ${o.volume}`),
  1043. this.lastUtter = Date.now(),
  1044. read()
  1045. }
  1046. )),
  1047. utterThis.addEventListener("end", (function(e) {
  1048. SAILogger.info("Speech has ended"),
  1049. resolve(void 0),
  1050. clearTimeout(timer)
  1051. }
  1052. )),
  1053. utterThis.addEventListener("pause", (function(e) {
  1054. SAILogger.verbose("Speech has paused", e)
  1055. }
  1056. )),
  1057. utterThis.addEventListener("resume", (function(e) {
  1058. SAILogger.verbose("Speech has resumed", e)
  1059. }
  1060. )),
  1061. utterThis.addEventListener("boundary", (function(e) {
  1062. SAILogger.verbose(`Speech reached boundary. CharIndex: ${e.charIndex}`, e)
  1063. }
  1064. )),
  1065. utterThis.addEventListener("mark", (function(e) {
  1066. SAILogger.info("Speech reached mark", e)
  1067. }
  1068. )),
  1069. this.synth.speak(utterThis),
  1070. this.lastUtterCharCount = formatText.length
  1071. }
  1072. ))
  1073. }
  1074. enableReadAloud() {
  1075. this.enabled = !0,
  1076. this.disableButton.classList.remove("disabled"),
  1077. this.updateStorage(),
  1078. document.querySelectorAll(".sai-repeat-button").forEach((e=>{
  1079. e.classList.remove("sai-disabled")
  1080. }
  1081. ))
  1082. }
  1083. disableReadAloud() {
  1084. this.queue = [],
  1085. this.queueIdle = !0,
  1086. this.synth.cancel(),
  1087. this.disableButton.classList.add("disabled"),
  1088. this.enabled = !1,
  1089. this.updateStorage(),
  1090. document.querySelectorAll(".sai-repeat-button").forEach((e=>{
  1091. e.classList.add("sai-disabled")
  1092. }
  1093. ))
  1094. }
  1095. updateStorage() {
  1096. window.localStorage.setItem(this.storageKey, this.enabled.toString())
  1097. }
  1098. setReadAloudFromStorage() {
  1099. const enabled = window.localStorage.getItem(this.storageKey);
  1100. enabled && (this.enabled = "true" === enabled,
  1101. this.enabled ? this.enableReadAloud() : this.disableReadAloud())
  1102. }
  1103. getText(markdown) {
  1104. const markdownElements = document.querySelectorAll(".text-base:not(.sai-skip) .markdown")
  1105. , children = (markdown || markdownElements[markdownElements.length - 1])?.children ?? [];
  1106. let text = "";
  1107. for (const child of children)
  1108. "PRE" !== child.nodeName && (text += child.textContent);
  1109. return text = text.replace(/`/g, "").replace(/\*/g, "").replace(/\"/g, "").replace(/\\n/g, "").replace(/\\t/g, "").replace(/\\b/g, "").replace(/(/g, " (").replace(/)/g, ") ").replace(/?/g, "? ").replace(/:/g, ": ").replace(/!/g, "! ").replace(/。/g, ". "),
  1110. text
  1111. }
  1112. reset() {
  1113. SAILogger.warn("RESET read aloud queue"),
  1114. this.queue = [],
  1115. this.lastRead = Date.now()
  1116. }
  1117. }
  1118. class SpeechImpl {
  1119. constructor(lang, errorMessage, speechCallback) {
  1120. this.lang = lang,
  1121. this.errorMessage = errorMessage,
  1122. this.transcript = "",
  1123. this.recognition = new webkitSpeechRecognition,
  1124. this.isRecording = !1,
  1125. this.recognition.continuous = !0,
  1126. this.recognition.interimResults = !0,
  1127. this.recognition.onstart = ()=>{}
  1128. ,
  1129. this.recognition.onresult = event=>{
  1130. let n = "";
  1131. for (let i = event.resultIndex; i < event.results.length; ++i)
  1132. event.results[i].isFinal ? this.isRecording && (this.transcript += event.results[i][0].transcript,
  1133. speechCallback(this.transcript)) : n += event.results[i][0].transcript;
  1134. this.isRecording && speechCallback(this.transcript + n)
  1135. }
  1136. ,
  1137. this.recognition.onerror = event=>{
  1138. let error = event.error;
  1139. "not-allowed" === event.error && (error = "The webpage is not allowed to access your microphone"),
  1140. "no-speech" === event.error && (error = "No sound from the microphone");
  1141. let html = `\n <span>\n Error from Voice Control:\n <br />\n ${error}\n <br /><br />\n <em style="font-size: 10px; font-weight: normal;">\n See voicecontrol.chat/support for help\n </em>\n </span>\n `;
  1142. this.errorMessage.write(html, 8e3),
  1143. SAILogger.error(`recognition.onerror ${event.error}`)
  1144. }
  1145. ,
  1146. this.recognition.onend = ()=>{
  1147. SAILogger.info("Ended"),
  1148. this.endCallback?.()
  1149. }
  1150. }
  1151. start(endCallback) {
  1152. SAILogger.info("Start"),
  1153. this.endCallback = endCallback,
  1154. this.recognition.lang = this.lang,
  1155. this.recognition.start(),
  1156. this.isRecording = !0
  1157. }
  1158. stop() {
  1159. SAILogger.info(`Stop: ${this.transcript}`),
  1160. this.isRecording = !1,
  1161. this.recognition.stop(),
  1162. this.endCallback = void 0
  1163. }
  1164. reset() {
  1165. this.isRecording = !1,
  1166. this.transcript = ""
  1167. }
  1168. setLang(langCode) {
  1169. this.lang = langCode
  1170. }
  1171. }
  1172. class SettingsImpl {
  1173. constructor(readAloud) {
  1174. this.readAloud = readAloud,
  1175. this.showCompactUi = "true" === window.localStorage.getItem("sai-compact-ui"),
  1176. this.appIsHidden = "true" === window.localStorage.getItem('"sai-hidden"'),
  1177. this.settingsView = document.createElement("div"),
  1178. this.settingsView.innerHTML = `\n <div id="sai-settings-header">\n <h3>Voice Control for ChatGPT</h3>\n <button class="sai-button" id="sai-close-settings">Close</button>\n </div>\n <div id="sai-settings-view-inner">\n <div class="sai-settings-col">\n <section>\n <h4 id="sai-settings-read-aloud-header">Read aloud speed: <span id="sai-read-aloud-speed"></span></h4>\n <div class="sai-slidecontainer">\n <input\n type="range"\n min="1"\n max="20"\n value="10"\n step="1"\n class="sai-slider"\n id="sai-popup-range-slider"\n />\n </div>\n </section>\n\n <section class="sai-settings-section">\n <h4>Voice preference</h4>\n <div id="sai-voice-settings"></div>\n <a href="https://voicecontrol.chat/install-voices" id="sai-settings-voice-link" target="_blank">\n Install more voices\n </a>\n </section>\n\n <section class="sai-settings-section">\n <h4>Display settings</h4>\n <p></p>\n <button id="sai-ui-toggle" class="sai-button">\n ${this.showCompactUi ? "Use classic interface" : "Use compact interface"}\n </button>\n <p></p>\n <button id="sai-display-toggle" class="sai-button">\n ${this.appIsHidden ? "Show " : "Hide "}\n Voice Control\n </button>\n </section>\n\n\n <section class="sai-settings-section">\n <h4>Need help or have a suggestion?</h4>\n <p>\n If you have trouble loading voices or need help troubleshooting please\n <a href="https://voicecontrol.chat/support" target="_blank">\n see the FAQ.\n </a>\n </p>\n\n <p>\n If you have suggestions on how to improve the extension please share your ideas\n <a href="https://forms.gle/BA3AU9LdApsZDBW28" target="_blank">\n here.\n </a>\n </p>\n </section>\n </div>\n <div class="sai-settings-col">\n <h4>Keyboard shortcuts</h4>\n\n <ul>\n <li>\n Press-and-hold <strong>SPACE</strong> (outside text input) to\n record, and release to submit\n </li>\n <li>\n Press <strong>ESC</strong> or <strong>Q</strong> to cancel a\n transcription\n </li>\n <li>\n Press <strong>E</strong> to stop and copy the transcription to the\n ChatGPT input field without submitting\n </li>\n </ul>\n\n <section class="sai-settings-section">\n <p><em>Upgrade your language learning experience with <a class="sai-link-talkio" href="https://talkio.ai" target="_blank">Talkio AI</a>,\n the premium version of this extension designed specifically for language learners.</em></p>\n </section>\n\n <section class="sai-settings-section">\n <p>\n The extension is created by <a href="https://twitter.com/theisof" target="_blank">Theis Frøhlich</a>\n <br />\n Please <a href="https://chrome.google.com/webstore/detail/voice-control-for-chatgpt/eollffkcakegifhacjnlnegohfdlidhn" target="_blank">leave a review</a>\n if you like this extension.\n </p>\n </section>\n </div>\n </div>`,
  1179. this.settingsView.id = "sai-settings-view",
  1180. this.settingsView.classList.add("sai-hide")
  1181. }
  1182. setupListeners() {
  1183. const sliderElement = document.getElementById("sai-popup-range-slider")
  1184. , speedElement = document.getElementById("sai-read-aloud-speed")
  1185. , closeSettingsElement = document.getElementById("sai-close-settings")
  1186. , displayToggleElement = document.getElementById("sai-display-toggle")
  1187. , uiToggleElement = document.getElementById("sai-ui-toggle");
  1188. if (!(sliderElement && speedElement && closeSettingsElement && displayToggleElement && uiToggleElement))
  1189. return void SAILogger.warn("settings element missing");
  1190. closeSettingsElement.onclick = ()=>{
  1191. this.settingsView.classList.add("sai-hide")
  1192. }
  1193. ,
  1194. displayToggleElement.onclick = ()=>{
  1195. document.body.classList.toggle("sai-hidden");
  1196. const hidden = window.localStorage.getItem('"sai-hidden"');
  1197. hidden && "true" === hidden ? (window.localStorage.setItem('"sai-hidden"', "false"),
  1198. this.appIsHidden = !1,
  1199. displayToggleElement.innerText = "Hide Voice Control") : (window.localStorage.setItem('"sai-hidden"', "true"),
  1200. this.appIsHidden = !0,
  1201. this.readAloud.disableReadAloud(),
  1202. displayToggleElement.innerText = "Show Voice Control")
  1203. }
  1204. ,
  1205. uiToggleElement.onclick = ()=>{
  1206. document.body.classList.toggle("sai-compact");
  1207. const showCompactUi = window.localStorage.getItem("sai-compact-ui");
  1208. showCompactUi && "true" === showCompactUi ? (window.localStorage.setItem("sai-compact-ui", "false"),
  1209. this.showCompactUi = !1,
  1210. uiToggleElement.innerText = "Compact interface") : (window.localStorage.setItem("sai-compact-ui", "true"),
  1211. this.showCompactUi = !0,
  1212. uiToggleElement.innerText = "Classic interface");
  1213. document.getElementById("sai-root")?.remove()
  1214. }
  1215. ;
  1216. const updateSpeed = speed=>{
  1217. speedElement.innerHTML = this.labelFromSpeedValue(speed);
  1218. sliderElement.value = speed,
  1219. window.localStorage.setItem("sai-voice-speed-v2", speed)
  1220. }
  1221. ;
  1222. sliderElement.oninput = event=>{
  1223. const t = event.target;
  1224. updateSpeed(t.value)
  1225. }
  1226. ;
  1227. const voiceSpeed = window.localStorage.getItem("sai-voice-speed-v2");
  1228. voiceSpeed && updateSpeed(voiceSpeed)
  1229. }
  1230. createVoiceSelector() {
  1231. const langCode = window.localStorage.getItem("sai-language") ?? "en-US"
  1232. , filteredVoices = window.speechSynthesis.getVoices().filter((v=>matchLanguageCode(v.lang, langCode))).reverse()
  1233. , preferenceVoice = window.localStorage.getItem("sai-voice-preference" + lang)
  1234. , selectElement = document.createElement("select");
  1235. selectElement.id = "sai-voice-selector",
  1236. selectElement.style.color = "black",
  1237. selectElement.style.width = "100%",
  1238. filteredVoices.forEach((v=>{
  1239. const element = document.createElement("option");
  1240. element.innerText = v.name,
  1241. element.value = v.voiceURI,
  1242. preferenceVoice === element.value && (element.selected = !0),
  1243. selectElement.appendChild(element)
  1244. }
  1245. )),
  1246. selectElement.onchange = event=>{
  1247. const t = event.target;
  1248. window.localStorage.setItem("sai-voice-preference" + lang, t.value)
  1249. }
  1250. ;
  1251. const voiceSettingsElement = document.getElementById("sai-voice-settings");
  1252. voiceSettingsElement && (voiceSettingsElement.innerHTML = "",
  1253. voiceSettingsElement.appendChild(selectElement))
  1254. }
  1255. labelFromSpeedValue(speed) {
  1256. return speed
  1257. }
  1258. }
  1259. class RepeatButton {
  1260. constructor(readAloud) {
  1261. const saiEnabled = "true" === window.localStorage.getItem("sai-read-aloud");
  1262. this.element = document.createElement("div"),
  1263. this.element.innerHTML = '<?xml version="1.0" encoding="utf-8"?>\n<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\n<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1657 2.14424C12.8728 2.50021 13 3.27314 13 3.7446V20.2561C13 20.7286 12.8717 21.4998 12.1656 21.8554C11.416 22.2331 10.7175 21.8081 10.3623 21.4891L4.95001 16.6248H3.00001C1.89544 16.6248 1.00001 15.7293 1.00001 14.6248L1 9.43717C1 8.3326 1.89543 7.43717 3 7.43717H4.94661L10.3623 2.51158C10.7163 2.19354 11.4151 1.76635 12.1657 2.14424ZM11 4.63507L6.00618 9.17696C5.82209 9.34439 5.58219 9.43717 5.33334 9.43717H3L3.00001 14.6248H5.33334C5.58015 14.6248 5.81823 14.716 6.00179 14.881L11 19.3731V4.63507Z" fill="#000000"/>\n<path d="M16.0368 4.73124C16.1852 4.19927 16.7368 3.88837 17.2688 4.03681C20.6116 4.9696 23 8.22106 23 12C23 15.779 20.6116 19.0304 17.2688 19.9632C16.7368 20.1117 16.1852 19.8007 16.0368 19.2688C15.8884 18.7368 16.1993 18.1852 16.7312 18.0368C19.1391 17.3649 21 14.9567 21 12C21 9.04332 19.1391 6.63512 16.7312 5.96321C16.1993 5.81477 15.8884 5.2632 16.0368 4.73124Z" fill="#000000"/>\n<path d="M16.2865 8.04192C15.7573 7.88372 15.2001 8.18443 15.0419 8.71357C14.8837 9.24271 15.1844 9.79992 15.7136 9.95812C16.3702 10.1544 17 10.9209 17 12C17 13.0791 16.3702 13.8456 15.7136 14.0419C15.1844 14.2001 14.8837 14.7573 15.0419 15.2865C15.2001 15.8156 15.7573 16.1163 16.2865 15.9581C17.9301 15.4667 19 13.8076 19 12C19 10.1924 17.9301 8.53333 16.2865 8.04192Z" fill="#000000"/>\n</svg>',
  1264. this.element.classList.add("sai-repeat-button"),
  1265. saiEnabled || this.element.classList.add("sai-disabled"),
  1266. this.element.onclick = ()=>{
  1267. const markdown = this.element.closest(".text-base")?.querySelector(".markdown");
  1268. markdown ? readAloud.repeat(markdown) : SAILogger.warn("Could not find text element to repeat")
  1269. }
  1270. }
  1271. }
  1272. class RepeatHandler {
  1273. constructor(readAloud) {
  1274. this.readAloud = readAloud;
  1275. }
  1276. injectRepeatButtons() {
  1277. document.querySelectorAll(".text-base .text-gray-400.flex.self-end.justify-center.mt-2.gap-2.visible .flex.gap-1").forEach((element=>{
  1278. if (element.querySelectorAll(".sai-repeat-button").length > 0)
  1279. return;
  1280. const repeatButton = new RepeatButton(this.readAloud);
  1281. repeatButton.appendChild(repeatButton.element)
  1282. }
  1283. ))
  1284. }
  1285. }
  1286. const defaultLanguageCode = staticLangSupportList[0][1];
  1287. class voiceControl {
  1288. constructor(reset=!1, waitForContent=!1) {
  1289. this.isRecording = !1,
  1290. this.language = defaultLanguageCode,
  1291. this.spaceIsDown = !1,
  1292. this.isCompact = "true" === window.localStorage.getItem("sai-compact-ui"),
  1293. SAILogger.info("Init app");
  1294. const chatGptInput = document.querySelector("textarea")
  1295. , chatGptInputParent = chatGptInput?.parentElement
  1296. , chatGptSubmitButton = chatGptInputParent?.querySelector("button");
  1297. if (!chatGptInput || !chatGptInputParent || !chatGptSubmitButton)
  1298. throw new Error("Missing elements");
  1299. this.chatGptInput = chatGptInput,
  1300. this.chatGptInputParent = chatGptInputParent,
  1301. this.chatGptSubmitButton = chatGptSubmitButton,
  1302. this.saiRoot = document.createElement("div"),
  1303. this.saiRoot.id = "sai-root",
  1304. this.saiInput = document.createElement("div"),
  1305. this.saiInput.className = s.classList.value + " sai-input",
  1306. this.saiInputWrapper = document.createElement("div"),
  1307. this.saiInputWrapper.id = "sai-input-wrapper",
  1308. this.saiInputWrapper.className = this.chatGptInputParent.classList.value + " is-idle",
  1309. this.saiInputWrapper.appendChild(this.saiInput),
  1310. this.saiRoot.appendChild(this.saiInputWrapper),
  1311. this.isCompact ? this.chatGptInputParent.before(this.saiRoot) : this.chatGptInputParent.after(this.saiRoot),
  1312. this.saiCancelMsg = document.createElement("div"),
  1313. this.saiCancelMsg.id = "sai-cancel-msg",
  1314. this.saiCancelMsg.innerHTML = "press esc to cancel",
  1315. this.saiInputWrapper.appendChild(this.saiCancelMsg),
  1316. this.saiRecordButton = document.createElement("div"),
  1317. this.saiRecordButton.id = "sai-speech-button",
  1318. this.saiRecordButton.innerHTML = '<?xml version="1.0" ?><svg baseProfile="tiny" height="24px" id="Layer_1" version="1.2" viewBox="0 0 24 24" width="24px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><path d="M12,16c2.206,0,4-1.795,4-4V6c0-2.206-1.794-4-4-4S8,3.794,8,6v6C8,14.205,9.794,16,12,16z"/><path d="M19,12v-2c0-0.552-0.447-1-1-1s-1,0.448-1,1v2c0,2.757-2.243,5-5,5s-5-2.243-5-5v-2c0-0.552-0.447-1-1-1s-1,0.448-1,1v2 c0,3.52,2.613,6.432,6,6.92V20H8c-0.553,0-1,0.447-1,1s0.447,1,1,1h8c0.553,0,1-0.447,1-1s-0.447-1-1-1h-3v-1.08 C16.387,18.432,19,15.52,19,12z"/></g></svg>',
  1319. this.saiInputWrapper.appendChild(this.saiRecordButton),
  1320. this.saiButtonWrapper = document.createElement("div"),
  1321. this.saiButtonWrapper.id = "sai-button-wrapper",
  1322. this.saiSettingsButton = document.createElement("div"),
  1323. this.saiSettingsButton.id = "sai-settings-button",
  1324. this.saiSettingsButton.innerHTML = '<?xml version="1.0" encoding="UTF-8"?>\n\x3c!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --\x3e\n<svg width="800px" height="800px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\n\n <title>/svg/ic-settings</title>\n <desc>Created with Sketch.</desc>\n <defs>\n\n</defs>\n <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">\n <g id="ic-settings" fill="#ffffff">\n <path d="M1,5 C1,4.44771525 1.44266033,4 1.99895656,4 L3.00104344,4 C3.55275191,4 4,4.44386482 4,5 C4,5.55228475 3.55733967,6 3.00104344,6 L1.99895656,6 C1.44724809,6 1,5.55613518 1,5 Z M12,5 C12,4.44771525 12.444837,4 12.9955775,4 L22.0044225,4 C22.5542648,4 23,4.44386482 23,5 C23,5.55228475 22.555163,6 22.0044225,6 L12.9955775,6 C12.4457352,6 12,5.55613518 12,5 Z M8,6 C7.44771525,6 7,5.55228475 7,5 C7,4.44771525 7.44771525,4 8,4 C8.55228475,4 9,4.44771525 9,5 C9,5.55228475 8.55228475,6 8,6 Z M8,8 C6.34314575,8 5,6.65685425 5,5 C5,3.34314575 6.34314575,2 8,2 C9.65685425,2 11,3.34314575 11,5 C11,6.65685425 9.65685425,8 8,8 Z M1,19 C1,18.4477153 1.44266033,18 1.99895656,18 L3.00104344,18 C3.55275191,18 4,18.4438648 4,19 C4,19.5522847 3.55733967,20 3.00104344,20 L1.99895656,20 C1.44724809,20 1,19.5561352 1,19 Z M12,19 C12,18.4477153 12.444837,18 12.9955775,18 L22.0044225,18 C22.5542648,18 23,18.4438648 23,19 C23,19.5522847 22.555163,20 22.0044225,20 L12.9955775,20 C12.4457352,20 12,19.5561352 12,19 Z M8,20 C7.44771525,20 7,19.5522847 7,19 C7,18.4477153 7.44771525,18 8,18 C8.55228475,18 9,18.4477153 9,19 C9,19.5522847 8.55228475,20 8,20 Z M8,22 C6.34314575,22 5,20.6568542 5,19 C5,17.3431458 6.34314575,16 8,16 C9.65685425,16 11,17.3431458 11,19 C11,20.6568542 9.65685425,22 8,22 Z M1,12 C1,11.4477153 1.4556644,11 1.99539757,11 L10.0046024,11 C10.5543453,11 11,11.4438648 11,12 C11,12.5522847 10.5443356,13 10.0046024,13 L1.99539757,13 C1.44565467,13 1,12.5561352 1,12 Z M19,12 C19,11.4477153 19.4433532,11 20.0093689,11 L21.9906311,11 C22.5480902,11 23,11.4438648 23,12 C23,12.5522847 22.5566468,13 21.9906311,13 L20.0093689,13 C19.4519098,13 19,12.5561352 19,12 Z M15,13 C14.4477153,13 14,12.5522847 14,12 C14,11.4477153 14.4477153,11 15,11 C15.5522847,11 16,11.4477153 16,12 C16,12.5522847 15.5522847,13 15,13 Z M15,15 C13.3431458,15 12,13.6568542 12,12 C12,10.3431458 13.3431458,9 15,9 C16.6568542,9 18,10.3431458 18,12 C18,13.6568542 16.6568542,15 15,15 Z" id="Combined-Shape">\n\n</path>\n </g>\n </g>\n</svg>',
  1325. this.saiSettingsButton.onclick = ()=>{
  1326. document.getElementById("sai-settings-view")?.classList.remove("sai-hide"),
  1327. this.settings.createVoiceSelector()
  1328. }
  1329. ,
  1330. this.saiErrorMessage = document.createElement("div"),
  1331. this.saiErrorMessage.id = "sai-error-message",
  1332. this.saiErrorMessage.innerHTML = "error",
  1333. this.saiErrorMessage.style.display = "none",
  1334. this.saiRoot.append(this.saiErrorMessage),
  1335. this.errorMessage = new ErrorMessage(this.saiErrorMessage),
  1336. this.speech = new SpeechImpl(this.language,this.errorMessage,this.speechCallback.bind(this)),
  1337. this.readAloud = new ReadAloudImpl(this.language,reset, waitForContent),
  1338. this.settings = new SettingsImpl(this.readAloud),
  1339. this.speechHandlers();
  1340. const langSelector = new LanguageSelector(this.setLanguage.bind(this), defaultLanguageCode);
  1341. this.saiButtonWrapper.appendChild(langSelector.element),
  1342. this.saiRoot.appendChild(this.saiButtonWrapper),
  1343. this.saiButtonWrapper.appendChild(this.readAloud.skipButton),
  1344. this.saiButtonWrapper.appendChild(this.readAloud.disableButton),
  1345. this.saiButtonWrapper.appendChild(this.saiSettingsButton),
  1346. this.saiRoot.appendChild(this.settings.settingsView),
  1347. this.settings.setupListeners(),
  1348. this.repeatHandler = new RepeatHandler(this.readAloud);
  1349. document.querySelectorAll("#sai-root").length > 1 && this.errorMessage.write("<span>\n Looks like Voice Control for ChatGPT is installed twice.\n Please go to chrome://extensions and disable one of the installations\n </span>\n ", 7e3)
  1350. }
  1351. keyDownHandler(event) {
  1352. const t = event.target;
  1353. if ("textarea" === t?.localName || "Space" !== event.code || this.spaceIsDown || (this.holdSpaceTimer = setTimeout((()=>{
  1354. SAILogger.info("Space start"),
  1355. this.startRecording(),
  1356. this.speech.start((()=>{
  1357. this.stopRecording()
  1358. }
  1359. ))
  1360. }
  1361. ), 250),
  1362. this.spaceIsDown = !0),
  1363. "textarea" !== t?.localName && "Space" === event.code && this.isRecording && this.isCompact && this.appToIdle(),
  1364. "textarea" === t?.localName || "Escape" !== event.code && "KeyQ" !== event.code || !this.isRecording || (SAILogger.info(`Pressed ${event.code}`),
  1365. this.appToIdle()),
  1366. ("Escape" === event.code || "KeyQ" === event.code) && !this.isRecording) {
  1367. SAILogger.info(`Pressed ${event.code}. Close settings`);
  1368. document.getElementById("sai-settings-view")?.classList.add("sai-hide")
  1369. }
  1370. "KeyE" === event.code && this.isRecording && (SAILogger.info("Pressed KeyE"),
  1371. this.chatGptInput.value = this.saiInput.innerText,
  1372. this.appToIdle()),
  1373. "textarea" !== t?.localName && "Enter" === event.code && this.isRecording && (SAILogger.info("Enter stop"),
  1374. this.submitToChatGPT(this.chatGptInput.value),
  1375. this.appToIdle())
  1376. }
  1377. keyUpHandler(event) {
  1378. this.spaceIsDown && "Space" === event.code && (this.isCompact || (SAILogger.info("Space stop"),
  1379. this.stopRecording()))
  1380. }
  1381. onSubmit() {
  1382. SAILogger.info("on Submit"),
  1383. this.appToIdle()
  1384. }
  1385. adjustCompactIconPos() {
  1386. if (!this.isCompact)
  1387. return;
  1388. const offsetHeight = this.chatGptInput.offsetHeight
  1389. , icon = document.querySelector("textarea + button.absolute.p-1.rounded-md");
  1390. icon && (icon.style.marginRight = offsetHeight > 30 ? "0" : "35px")
  1391. }
  1392. speechHandlers() {
  1393. this.spaceIsDown = !1,
  1394. this.saiInputWrapper.onclick = ()=>{
  1395. this.isRecording ? (this.speech.stop(),
  1396. this.stopRecording()) : (this.startRecording(),
  1397. this.speech.start((()=>{
  1398. this.stopRecording()
  1399. }
  1400. )))
  1401. }
  1402. }
  1403. startRecording() {
  1404. this.isRecording = !0,
  1405. this.saiInputWrapper.classList.remove("is-idle")
  1406. }
  1407. stopRecording() {
  1408. if (this.isCompact)
  1409. this.chatGptInput.dispatchEvent(new Event("input",{
  1410. bubbles: !0
  1411. }));
  1412. else {
  1413. const text = this.saiInput.innerText;
  1414. this.submitToChatGPT(text)
  1415. }
  1416. this.appToIdle()
  1417. }
  1418. submitToChatGPT(text) {
  1419. text.length > 0 && (this.chatGptInput.value = text,
  1420. this.chatGptInput.dispatchEvent(new Event("input",{
  1421. bubbles: !0
  1422. })),
  1423. this.chatGptSubmitButton.click())
  1424. }
  1425. appToIdle() {
  1426. this.speech.stop(),
  1427. this.speech.reset(),
  1428. this.isRecording = !1,
  1429. this.saiInput.innerText = "",
  1430. this.saiInputWrapper.classList.add("is-idle"),
  1431. this.spaceIsDown = !1,
  1432. clearTimeout(this.holdSpaceTimer)
  1433. }
  1434. speechCallback(transcript) {
  1435. this.isCompact ? transcript.length > 0 && (this.chatGptInput.value = transcript,
  1436. this.chatGptInput.dispatchEvent(new Event("input",{
  1437. bubbles: !0
  1438. }))) : this.saiInput.innerText = transcript
  1439. }
  1440. setLanguage(langCode) {
  1441. this.language = langCode,
  1442. this.readAloud.setLang(langCode),
  1443. this.speech.setLang(langCode)
  1444. }
  1445. }
  1446. SAILogger.setup();
  1447. window.localStorage.getItem("sai-voice-speed-v2") || window.localStorage.setItem("sai-voice-speed-v2", "10");
  1448. "true" === window.localStorage.getItem('"sai-hidden"') && document.body.classList.add("sai-hidden");
  1449. function initApp() {
  1450. const fnCheckPath = () => /^\/c\/(.*)$/.test(window.location.pathname);
  1451. let vc = new voiceControl(!1, fnCheckPath())
  1452. , flag = !1;
  1453. const keydownListener = event=>{
  1454. vc.keyDownHandler(event)
  1455. }
  1456. , keyupListener = event=>{
  1457. vc.keyUpHandler(event)
  1458. }
  1459. , submitListener = event=>{
  1460. "Enter" === event.code && vc.onSubmit()
  1461. }
  1462. , clickListener = ()=>{
  1463. vc.onSubmit()
  1464. }
  1465. ;
  1466. document.addEventListener("keydown", keydownListener),
  1467. document.addEventListener("keyup", keyupListener);
  1468. const buttonSelector = "form button.absolute.p-1.rounded-md.text-gray-500";
  1469. function callback(observerCallback) {
  1470. const root = document.getElementById("sai-root")
  1471. , textarea = document.querySelector("textarea");
  1472. vc?.adjustCompactIconPos(),
  1473. vc?.repeatHandler?.injectRepeatButtons(),
  1474. flag && textarea && (SAILogger.info("Re-init app"),
  1475. root && root.remove(),
  1476. vc = new voiceControl(!0,fnCheckPath()),
  1477. flag = !1,
  1478. document.addEventListener("keydown", keydownListener),
  1479. document.addEventListener("keyup", keyupListener),
  1480. document.querySelector("textarea")?.addEventListener("keyup", submitListener),
  1481. document.querySelector(buttonSelector)?.addEventListener("click", clickListener)),
  1482. root && textarea || (SAILogger.warn("App removed"),
  1483. root && root.remove(),
  1484. vc.readAloud.reset(),
  1485. document.removeEventListener("keydown", keydownListener),
  1486. document.removeEventListener("keyup", keyupListener),
  1487. document.querySelector("textarea")?.removeEventListener("keyup", submitListener),
  1488. document.querySelector(buttonSelector)?.removeEventListener("click", clickListener),
  1489. flag = !0),
  1490. root && vc.readAloud.observerCallback(observerCallback)
  1491. }
  1492. document.querySelector("textarea")?.addEventListener("keyup", keyupListener),
  1493. document.querySelector(buttonSelector)?.addEventListener("click", clickListener);
  1494. new MutationObserver(callback).observe(document.body, {
  1495. childList: !0,
  1496. subtree: !0,
  1497. characterData: !0
  1498. }),
  1499. setInterval((()=>{
  1500. callback([])
  1501. }
  1502. ), 3500)
  1503. }
  1504. "true" === window.localStorage.getItem("sai-compact-ui") && document.body.classList.add("sai-compact"),
  1505. // chrome.runtime.onMessage.addListener(((message, sender, sendResponse)=>("sai-on-chatgpt-message" === message.key && sendResponse({
  1506. // value: "yes-we-are-here"
  1507. // }),
  1508. // !0)));
  1509. document.querySelector("textarea") ? initApp() : setTimeout((()=>{
  1510. initApp()
  1511. }
  1512. ), 2e3);
  1513. })();