KingNish commited on
Commit
c6d6751
1 Parent(s): 5e2ef45

Update script2.js

Browse files
Files changed (1) hide show
  1. script2.js +582 -1
script2.js CHANGED
@@ -1 +1,582 @@
1
- eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('9 X=1a.1b(\'X\');9 2U=1a.1b(\'4r\');9 1p=1a.1b(\'4s\');9 4t=1a.1b(\'4u\');9 2V=1a.1b(\'4v\');9 2W=1a.1b(\'4w\');9 Y=1a.1b(\'4x\');9 2X=1a.1b(\'1j\');e y;e 2s=Q;e 2t=0;e 1c=\'\';e Z=D;e U=D;e E=D;e 1k=Q;e 4y=\'\';e 2Y=Q;e 2u="";e 1q=Q;9 4z=1l;9 3v="4A://4B.4C.4D/4E/4F/4G";9 3w=11;e v=Q;e 1d=[];e 2d=[];9 1m=q 2Z();9 1n=q 2Z();9 3x=10;9 3y=4H;e z=[];9 2e=q 2Z();9 3A=4I;9 2f=l=>l.1r().4J().30(/[^\\w\\s]/g,\'\');9 31=(1e,p,2v,3B)=>`${1e}-${p}-${32.33(2v)}-${3B}`;9 3C=(l,p)=>{9 1e=2f(l);9 j=31(1e,p,z,1p.R);a(1n.2w(j)||1m.2w(j))1o;2d.1s({l:l.1r(),p,j});34()};9 34=1t()=>{35(2d.t>0&&1n.4K<3x){9{l,p,j}=2d.3D();9 1u=q 3E();1n.36(j,1u);2g{9 x=u 37(`/3F?l=${38(l)}&2v=${32.33(z)}&3G=${1p.R}`,{39:\'3a\',3H:{\'3I\':\'F/13-2x\',\'2h-3J\':\'3K/3L\'},1v:1u.1v});a(!x.1w)2i q 1f(\'3b x 3c 2y 1w\');9 3d=u 3e(x.3M,p,1u.1v);a(3d)1m.36(j,{S:3d,2z:15.17()})}2j(k){a(k.3N!==\'2A\')T.k("1f 4L 18:",k)}2B{1n.2C(j);34()}}};9 3e=1t(2D,p,2E)=>{9 1x=2D.3O();9 2F=q 3P("3Q-8");e 1g="";2g{35(C){9{2G,R}=u 1x.3R();a(2G)3f;a(2E.2H)2i q 3S(\'3T 2H\',\'2A\');9 2I=2F.3U(R,{2x:C});1g+=2I;9 V=1g.3V(\'\\n\');2J(e i=0;i<V.t-1;i++){9 b=V[i];a(b.3g(\'3W: \')){9 m=b.3X(6).1r();a(m){9 A=u 2k(m,p);1o A}}}1g=V[V.t-1]}}2j(k){T.k("1f 2K 3e:",k)}2B{1x.3Y()}1o Q};9 1y=1t()=>{a(1d.t>0){9 3Z=1d.3D();9 18=q 4M(3Z.S);W();a(E){y.40();E=D;X.1z=`<o 1A="1B://1C.1D.1E/1F/o"G="24"1G="24"1H="0 0 24 24"1I="1J"h="1K"h-G="2"h-1L="H"h-1M="H"><c d="1N 1O 3 0 0 0-3 1P 3 0 0 0 6 1Q 3 0 0 0-3-3z"></c><c d="1R 1S 7 0 0 1-14 1T-2"></c><b I="12"J="19"K="12"L="23"></b><b I="8"J="23"K="16"L="23"></b></o>41 1h`}9 42=q 43(2l=>{18.4N=2l;18.44=2l});a(v){v.45();v.46=0}v=18;u 18.4O();u 42;1y()}M{W();47(()=>{a(!E){y.2m();E=C;X.1z=`<o 1A="1B://1C.1D.1E/1F/o"G="24"1G="24"1H="0 0 24 24"1I="1J"h="1K"h-G="2"h-1L="H"h-1M="H"><c d="3h 3i-3j"></c><c d="1N 1O 3 0 0 0-3 1P 3 0 0 0 6 1Q 3 0 0 0-3-3z"></c><c d="1R 1S 7 0 0 1-14 1T-2"></c><b I="12"J="19"K="12"L="23"></b><b I="8"J="23"K="16"L="23"></b></o>3k 2n`}},4P)}};9 2k=1t(F,p)=>{9 48=2f(F);9 j=`${48}-${p}`;a(2e.2w(j)){9 1U=2e.49(j);a(15.17()-1U.2z<3A){1o 1U.S}M{2e.2C(j)}}2g{9 x=u 37(`${3v}?p=${p}&F=${38(F)}`,{39:\'3a\'});a(!x.1w)2i q 1f(\'3b x 3c 2y 1w\');9 4a=u x.4Q();9 A=4R.4S(4a);2e.36(j,{S:A,2z:15.17()});1o A}2j(k){T.k("1f 4T 4U 18:",k);1o Q}};9 3l=1t(l)=>{T.2o("4V l f 1h:",l);Z=C;W();2t=15.17();1q=Q;9 1e=2f(l);9 j=31(1e,1p.R,z,1p.R);a(1m.2w(j)){9 1U=1m.49(j);a(15.17()-1U.2z<3y){9 4b=1U.S;1d.1s({S:4b,2L:C});1y()}M{1m.2C(j)}}1k=q 3E();9 S=`/3F?l=${38(l)}&3G=${1p.R}&2v=${32.33(z)}`;2g{9 x=u 37(S,{39:\'3a\',3H:{\'3I\':\'F/13-2x\',\'2h-3J\':\'3K/3L\'},1v:1k.1v});a(!x.1w){a(x.4c===4W){T.2o("4X 4Y 4Z, 50 2K 1 51...");u q 43(2l=>47(2l,52));u 3l(l);1o}2i q 1f(`3b x 3c 2y 1w:${x.4c}`)}T.2o("53 18 x 54");u 3m(x.3M,2U.R,1k.1v)}2j(k){a(k.3N!==\'2A\'){T.k("1f 55 l f 1h:",k)}}2B{Z=D;W()}};9 3m=1t(2D,p,2E)=>{9 1x=2D.3O();9 2F=q 3P("3Q-8");e 1g="";e 3n=0;e 1V="";e 2p="";e 2M="";2g{35(C){9{2G,R}=u 1x.3R();a(2G)3f;a(2E.2H)2i q 3S(\'3T 2H\',\'2A\');a(U){2N(\'4d 56 57\');3f}9 2I=2F.3U(R,{2x:C});1g+=2I;9 V=1g.3V(\'\\n\');2J(e i=0;i<V.t-1;i++){9 b=V[i];a(b.3g(\'3W: \')){9 m=b.3X(6).1r();a(m){a(!1q)1q=15.17();1V+=m+" ";2p+=m+" ";2X.m=1V;a(3n<2){9 A=u 2k(m,p);a(A){1d.1s({S:A,2L:D});a(!v)1y()}2M+=m+" ";3n++}M{e 1W=2p.30(2M,\'\').1r();a(1W.t>=3w){9 A=u 2k(1W,p);a(A){1d.1s({S:A,2L:D});a(!v)1y()}2p=""}}}}}1g=V[V.t-1]}}2j(k){T.k("1f 2K 3m:",k)}2B{1x.3Y();e 1W=2p.30(2M,\'\').1r();a(1W!==""){9 A=u 2k(1W,p);a(A){1d.1s({S:A,2L:D});a(!v)1y()}}a(1V!==\'\'){3o(\'4e\',1V);1V=\'\'}}};9 W=(4f=Q)=>{2W.m=U?"4g: 4h":"4g: 4i";2W.2q=U?"1X 1Y-1Z 20-4 21-2 F-22 25 26-27 28-29 2a-11 N-O-f-r B-2O-1i f-2O-2P 2b:N-O-f-r B-2O-1l f-2O-2c":"1X 1Y-1Z 20-4 21-2 F-22 25 26-27 28-29 2a-11 N-O-f-r B-P-11 f-P-1i 2Q:B-P-2c 2Q:f-P-4j 2b:N-O-f-r B-P-1i f-P-1l";a(Z&&!v){Y.m="1h: 58...";Y.2q="1X 1Y-1Z 20-4 21-2 F-22 25 26-27 28-29 2a-11 N-O-f-r B-2R-1i f-2R-2P 2b:N-O-f-r B-2R-1l f-2R-2c"}M a(v&&!U){Y.m=4f||"1h: 4h";Y.2q="1X 1Y-1Z 20-4 21-2 F-22 25 26-27 28-29 2a-11 N-O-f-r B-2S-1i f-2S-2P 2b:N-O-f-r B-2S-1l f-2S-2c"}M a(U){Y.m="1h: 2n";Y.2q="1X 1Y-1Z 20-4 21-2 F-22 25 26-27 28-29 2a-11 N-O-f-r B-2T-1i f-2T-2P 2b:N-O-f-r B-2T-1l f-2T-2c"}M{Y.m="1h: 4i";Y.2q="1X 1Y-1Z 20-4 21-2 F-22 25 26-27 28-29 2a-11 N-O-f-r B-P-11 f-P-1i 2Q:B-P-2c 2Q:f-P-4j 2b:N-O-f-r B-P-1i f-P-1l"}};a(\'4k\'2K 59){y=q 4k();5a.5b(y,{5c:C,5d:C,5e:\'5f-5g\',5h:3});y.5i=()=>{T.2o("3p 4l 5j");1c=\'\';U=C;2Y=15.17();W();X.1z=`<o 1A="1B://1C.1D.1E/1F/o"G="24"1G="24"1H="0 0 24 24"1I="1J"h="1K"h-G="2"h-1L="H"h-1M="H"><c d="3h 3i-3j"></c><c d="1N 1O 3 0 0 0-3 1P 3 0 0 0 6 1Q 3 0 0 0-3-3z"></c><c d="1R 1S 7 0 0 1-14 1T-2"></c><b I="12"J="19"K="12"L="23"></b><b I="8"J="23"K="16"L="23"></b></o>3k 2n`};y.5k=(13)=>{e 2r=\'\';2J(e i=13.5l;i<13.3q.t;i++){9 1j=13.3q[i][0].1j;a(13.3q[i].5m){1c+=1j;2N(\'5n\');3r(1c);1c=\'\';U=D;W();2t=15.17()}M{2r+=1j;U=C;2Y=15.17();W();a(2r.t>2u.t+5){4m(2u)}2u=2r;3C(2r,2U.R)}}};y.44=(13)=>{T.k(\'3p 4l k:\',13.k);a(E)y.2m()};y.5o=()=>{U=D;W();a(!Z&&1c!==\'\'){3r(1c);1c=\'\'}a(E)y.2m()};X.5p(\'5q\',()=>{a(E&&!Z){y.40();E=D;X.1z=`<o 1A="1B://1C.1D.1E/1F/o"G="24"1G="24"1H="0 0 24 24"1I="1J"h="1K"h-G="2"h-1L="H"h-1M="H"><c d="1N 1O 3 0 0 0-3 1P 3 0 0 0 6 1Q 3 0 0 0-3-3z"></c><c d="1R 1S 7 0 0 1-14 1T-2"></c><b I="12"J="19"K="12"L="23"></b><b I="8"J="23"K="16"L="23"></b></o>5r 2n`}M a(E&&Z||v){2N(\'5s 5t\');y.2m();E=C;X.1z=`<o 1A="1B://1C.1D.1E/1F/o"G="24"1G="24"1H="0 0 24 24"1I="1J"h="1K"h-G="2"h-1L="H"h-1M="H"><c d="1N 1O 3 0 0 0-3 1P 3 0 0 0 6 1Q 3 0 0 0-3-3z"></c><c d="1R 1S 7 0 0 1-14 1T-2"></c><b I="12"J="19"K="12"L="23"></b><b I="8"J="23"K="16"L="23"></b></o>41 1h`}M{y.2m();E=C;X.1z=`<o 1A="1B://1C.1D.1E/1F/o"G="24"1G="24"1H="0 0 24 24"1I="1J"h="1K"h-G="2"h-1L="H"h-1M="H"><c d="3h 3i-3j"></c><c d="1N 1O 3 0 0 0-3 1P 3 0 0 0 6 1Q 3 0 0 0-3-3z"></c><c d="1R 1S 7 0 0 1-14 1T-2"></c><b I="12"J="19"K="12"L="23"></b><b I="8"J="23"K="16"L="23"></b></o>3k 2n`}})}M{5u(\'5v 5w 5x 2y 5y 5z 5A 3p 5B.\')}9 3o=(3s,2h)=>{a(z.t>0&&z[z.t-1].3s===\'4e\'&&z[z.t-1].2h===""){z.5C()}z.1s({3s,2h});a(z.t>6)z.5D(0,2)};9 3r=(1j)=>{9 3t=1j.5E();a(3t!==\'\'&&!Z){2s=3t;3l(2s);3o(\'4d\',2s);2X.m=\'\'}};9 2N=(3u=\'5F\')=>{T.2o(`5G 18(3u:${3u})...`);a(v){v.45();v.46=0;v=Q}1d.t=0;Z=D;a(1k){1k.4n();1k=Q}1m.5H();2d.t=0;W()};9 4m=(l)=>{9 1e=2f(l);2J(9[j,1u]5I 1n){a(j.3g(1e)){1u.4n();1n.2C(j)}}};9 4o=()=>{a(1q){9 4p=1q-2t;2V.m=`4q:${4p}5J`}M{2V.m="4q: 5K"}};5L(4o,5M);',62,359,'|||||||||const|if|line|path||let|to||stroke||cacheKey|error|query|textContent||svg|voice|new|||length|await|currentAudio||response|speechRecognizer|conversationHistory|audioUrl|from|true|false|isSpeechRecognitionActive|text|width|round|x1|y1|x2|y2|else|bg|gradient|gray|null|value|url|console|isUserSpeaking|lines|updateActivityIndicators|startStopButton|aiActivityIndicator|isRequestInProgress||300||event||Date||now|audio||document|getElementById|completeTranscript|audioPlaybackQueue|normalizedQuery|Error|buffer|AI|400|transcript|requestAbortController|500|prefetchCache|pendingPrefetchRequests|return|modelSelectionDropdown|firstResponseTextTimestamp|trim|push|async|abortController|signal|ok|reader|playNextAudio|innerHTML|xmlns|http|www|w3|org|2000|height|viewBox|fill|none|currentColor|linecap|linejoin|M12|1a3|3v8a3|0V4a3|M19|10v2a7|0v|cachedData|fullResponseText|unsentTextChunk|indicator|rounded|full|px|py|white|||flex|items|center|transition|colors|duration|hover|700|prefetchQueue|audioCache|normalizeQueryText|try|content|throw|catch|generateTextToSpeechAudio|resolve|start|Listening|log|textChunk|className|interimTranscript|activeQuery|queryStartTime|prefetchTextQuery|history|has|stream|not|timestamp|AbortError|finally|delete|responseStream|abortSignal|decoder|done|aborted|chunk|for|in|isPrefetched|sentText|interruptAudioPlayback|blue|600|dark|purple|green|yellow|voiceSelectionDropdown|responseTimeDisplay|userActivityIndicator|transcriptDiv|lastUserSpeechTimestamp|Map|replace|generateCacheKey|JSON|stringify|processPrefetchQueue|while|set|fetch|encodeURIComponent|method|GET|Network|was|firstAudioUrl|handleStreamingResponseForPrefetch|break|startsWith|M9|9h6v6h|6z|Stop|sendQueryToAI|handleStreamingResponse|initialChunksSent|addToConversationHistory|Speech|results|processSpeechTranscript|role|trimmedTranscript|reason|TEXT_TO_SPEECH_API_ENDPOINT|CHUNK_SIZE|MAX_PREFETCH_REQUESTS|prefetchCacheExpiration||audioCacheExpiration|modelName|prefetchFirstAudioChunk|shift|AbortController|stream_audio|model|headers|accept|type|application|json|body|name|getReader|TextDecoder|utf|read|DOMException|Request|decode|split|data|substring|releaseLock|audioData|stop|Interrupt|audioPromise|Promise|onerror|pause|currentTime|setTimeout|normalizedText|get|audioBlob|prefetchedAudioUrl|status|user|assistant|state|User|Speaking|Idle|800|webkitSpeechRecognition|recognition|cancelPrefetchRequests|abort|updateLatency|latency|Latency|voiceSelect|modelSelect|noiseSuppressionCheckbox|noiseSuppression|responseTime|userIndicator|aiIndicator|partialTranscript|USER_SPEECH_INTERRUPT_DELAY|https|api|streamelements|com|kappa|v2|speech|60000|3600000|toLowerCase|size|prefetching|Audio|onended|play|100|blob|URL|createObjectURL|generating|TTS|Sending|429|Rate|limit|hit|retrying|second|1000|Streaming|received|sending|is|speaking|Processing|window|Object|assign|continuous|interimResults|language|en|US|maxAlternatives|onstart|started|onresult|resultIndex|isFinal|final|onend|addEventListener|click|Start|button|interrupt|alert|Your|browser|does|support|the|Web|API|pop|splice|trimStart|unknown|Interrupting|clear|of|ms|0ms|setInterval|200'.split('|'),0,{}))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const startStopButton = document.getElementById('startStopButton');
2
+ const voiceSelectionDropdown = document.getElementById('voiceSelect');
3
+ const modelSelectionDropdown = document.getElementById('modelSelect');
4
+ const noiseSuppressionCheckbox = document.getElementById('noiseSuppression');
5
+ const responseTimeDisplay = document.getElementById('responseTime');
6
+ const userActivityIndicator = document.getElementById('userIndicator');
7
+ const aiActivityIndicator = document.getElementById('aiIndicator');
8
+ const transcriptDiv = document.getElementById('transcript');
9
+
10
+ let speechRecognizer;
11
+ let activeQuery = null;
12
+ let queryStartTime = 0;
13
+ let completeTranscript = '';
14
+ let isRequestInProgress = false;
15
+ let isUserSpeaking = false;
16
+ let isSpeechRecognitionActive = false;
17
+ let requestAbortController = null;
18
+ let partialTranscript = '';
19
+ let lastUserSpeechTimestamp = null;
20
+ let prefetchTextQuery = "";
21
+ let firstResponseTextTimestamp = null;
22
+
23
+ // Configuration
24
+ const USER_SPEECH_INTERRUPT_DELAY = 500;
25
+ const TEXT_TO_SPEECH_API_ENDPOINT = "https://api.streamelements.com/kappa/v2/speech";
26
+ const CHUNK_SIZE = 300;
27
+
28
+ // Audio Management
29
+ let currentAudio = null;
30
+ let audioPlaybackQueue = [];
31
+ let prefetchQueue = [];
32
+
33
+ // Enhanced Prefetching and Caching
34
+ const prefetchCache = new Map();
35
+ const pendingPrefetchRequests = new Map();
36
+ const MAX_PREFETCH_REQUESTS = 10;
37
+ const prefetchCacheExpiration = 60000; // 1 minute
38
+
39
+ // Global Conversation History
40
+ let conversationHistory = [];
41
+
42
+ // Audio Caching
43
+ const audioCache = new Map();
44
+ const audioCacheExpiration = 3600000; // 1 hour
45
+
46
+ // Normalize query text
47
+ const normalizeQueryText = query => query.trim().toLowerCase().replace(/[^\w\s]/g, '');
48
+
49
+ // Generate a cache key
50
+ const generateCacheKey = (normalizedQuery, voice, history, modelName) =>
51
+ `${normalizedQuery}-${voice}-${JSON.stringify(history)}-${modelName}`;
52
+
53
+ // Prefetch and cache the first TTS audio chunk
54
+ const prefetchFirstAudioChunk = (query, voice) => {
55
+ const normalizedQuery = normalizeQueryText(query);
56
+ const cacheKey = generateCacheKey(normalizedQuery, voice, conversationHistory, modelSelectionDropdown.value);
57
+
58
+ if (pendingPrefetchRequests.has(cacheKey) || prefetchCache.has(cacheKey)) return;
59
+
60
+ prefetchQueue.push({ query:query.trim(), voice, cacheKey });
61
+ processPrefetchQueue();
62
+ };
63
+
64
+ // Process the prefetch queue
65
+ const processPrefetchQueue = async () => {
66
+ while (prefetchQueue.length > 0 && pendingPrefetchRequests.size < MAX_PREFETCH_REQUESTS) {
67
+ const { query, voice, cacheKey } = prefetchQueue.shift();
68
+ const abortController = new AbortController();
69
+ pendingPrefetchRequests.set(cacheKey, abortController);
70
+
71
+ const url = '/stream_text';
72
+ const requestBody = {
73
+ query: query,
74
+ history: JSON.stringify(conversationHistory),
75
+ model: modelSelectionDropdown.value
76
+ };
77
+
78
+ try {
79
+ const response = await fetch(url, {
80
+ method: 'POST',
81
+ headers: {
82
+ 'Accept': 'text/event-stream',
83
+ 'Content-Type': 'application/json'
84
+ },
85
+ body: JSON.stringify(requestBody),
86
+ signal: abortController.signal
87
+ });
88
+
89
+ if (!response.ok) throw new Error('Network response was not ok');
90
+
91
+ const firstAudioUrl = await handleStreamingResponseForPrefetch(response.body, voice, abortController.signal);
92
+
93
+ if (firstAudioUrl) prefetchCache.set(cacheKey, { url: firstAudioUrl, timestamp: Date.now() });
94
+
95
+ } catch (error) {
96
+ if (error.name !== 'AbortError') console.error("Error prefetching audio:", error);
97
+ } finally {
98
+ pendingPrefetchRequests.delete(cacheKey);
99
+ processPrefetchQueue();
100
+ }
101
+ }
102
+ };
103
+
104
+ // Handle the streaming response for prefetching
105
+ const handleStreamingResponseForPrefetch = async (responseStream, voice, abortSignal) => {
106
+ const reader = responseStream.getReader();
107
+ const decoder = new TextDecoder("utf-8");
108
+ let buffer = "";
109
+
110
+ try {
111
+ while (true) {
112
+ const { done, value } = await reader.read();
113
+ if (done) break;
114
+ if (abortSignal.aborted) throw new DOMException('Request aborted', 'AbortError');
115
+
116
+ const chunk = decoder.decode(value, { stream: true });
117
+ buffer += chunk;
118
+ const lines = buffer.split('\n');
119
+
120
+ for (let i = 0; i < lines.length - 1; i++) {
121
+ const line = lines[i];
122
+ if (line.startsWith('data: ')) {
123
+ const textContent = line.substring(6).trim();
124
+ if (textContent) {
125
+ const audioUrl = await generateTextToSpeechAudio(textContent, voice);
126
+ return audioUrl;
127
+ }
128
+ }
129
+ }
130
+
131
+ buffer = lines[lines.length - 1];
132
+ }
133
+ } catch (error) {
134
+ console.error("Error in handleStreamingResponseForPrefetch:", error);
135
+ } finally {
136
+ reader.releaseLock();
137
+ }
138
+
139
+ return null;
140
+ };
141
+
142
+ // Play audio from the queue
143
+ const playNextAudio = async () => {
144
+ if (audioPlaybackQueue.length > 0) {
145
+ const audioData = audioPlaybackQueue.shift();
146
+ const audio = new Audio(audioData.url);
147
+ updateActivityIndicators();
148
+
149
+ // Pause speech recognition if it's active
150
+ if (isSpeechRecognitionActive) {
151
+ speechRecognizer.stop();
152
+ isSpeechRecognitionActive = false;
153
+ startStopButton.innerHTML = `
154
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
155
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
156
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
157
+ <line x1="12" y1="19" x2="12" y2="23"></line>
158
+ <line x1="8" y1="23" x2="16" y2="23"></line>
159
+ </svg>
160
+ Interrupt AI
161
+ `;
162
+ }
163
+
164
+ const audioPromise = new Promise(resolve => {
165
+ audio.onended = resolve;
166
+ audio.onerror = resolve;
167
+ });
168
+ if (currentAudio) {
169
+ currentAudio.pause();
170
+ currentAudio.currentTime = 0;
171
+ }
172
+
173
+ currentAudio = audio;
174
+ await audio.play();
175
+ await audioPromise;
176
+ playNextAudio();
177
+ } else {
178
+ updateActivityIndicators();
179
+
180
+ // Resume speech recognition if it was paused with a delay
181
+ setTimeout(() => {
182
+ if (!isSpeechRecognitionActive) {
183
+ speechRecognizer.start();
184
+ isSpeechRecognitionActive = true;
185
+ startStopButton.innerHTML = `
186
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
187
+ <path d="M9 9h6v6h-6z"></path>
188
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
189
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
190
+ <line x1="12" y1="19" x2="12" y2="23"></line>
191
+ <line x1="8" y1="23" x2="16" y2="23"></line>
192
+ </svg>
193
+ Stop Listening
194
+ `;
195
+ }
196
+ }, 100);
197
+ }
198
+ };
199
+
200
+ // Generate Text-to-Speech audio with caching
201
+ const generateTextToSpeechAudio = async (text, voice) => {
202
+ const normalizedText = normalizeQueryText(text);
203
+ const cacheKey = `${normalizedText}-${voice}`;
204
+
205
+ if (audioCache.has(cacheKey)) {
206
+ const cachedData = audioCache.get(cacheKey);
207
+ if (Date.now() - cachedData.timestamp < audioCacheExpiration) {
208
+ return cachedData.url;
209
+ } else {
210
+ audioCache.delete(cacheKey);
211
+ }
212
+ }
213
+
214
+ try {
215
+ const response = await fetch(`${TEXT_TO_SPEECH_API_ENDPOINT}?voice=${voice}&text=${encodeURIComponent(text)}`, { method: 'GET' });
216
+ if (!response.ok) throw new Error('Network response was not ok');
217
+ const audioBlob = await response.blob();
218
+ const audioUrl = URL.createObjectURL(audioBlob);
219
+
220
+ audioCache.set(cacheKey, { url: audioUrl, timestamp: Date.now() });
221
+ return audioUrl;
222
+ } catch (error) {
223
+ console.error("Error generating TTS audio:", error);
224
+ return null;
225
+ }
226
+ };
227
+
228
+ // Send a query to the AI
229
+ const sendQueryToAI = async (query) => {
230
+ console.log("Sending query to AI:", query);
231
+ isRequestInProgress = true;
232
+ updateActivityIndicators();
233
+ queryStartTime = Date.now();
234
+ firstResponseTextTimestamp = null;
235
+
236
+ const normalizedQuery = normalizeQueryText(query);
237
+ const cacheKey = generateCacheKey(normalizedQuery, modelSelectionDropdown.value, conversationHistory, modelSelectionDropdown.value);
238
+
239
+ if (prefetchCache.has(cacheKey)) {
240
+ const cachedData = prefetchCache.get(cacheKey);
241
+ if (Date.now() - cachedData.timestamp < prefetchCacheExpiration) {
242
+ const prefetchedAudioUrl = cachedData.url;
243
+ audioPlaybackQueue.push({ url: prefetchedAudioUrl, isPrefetched: true });
244
+ playNextAudio();
245
+ } else {
246
+ prefetchCache.delete(cacheKey);
247
+ }
248
+ }
249
+
250
+ requestAbortController = new AbortController();
251
+
252
+ const url = '/stream_text';
253
+ const requestBody = {
254
+ query: query,
255
+ history: JSON.stringify(conversationHistory),
256
+ model: modelSelectionDropdown.value
257
+ };
258
+
259
+ try {
260
+ const response = await fetch(url, {
261
+ method: 'POST',
262
+ headers: {
263
+ 'Accept': 'text/event-stream',
264
+ 'Content-Type': 'application/json'
265
+ },
266
+ body: JSON.stringify(requestBody),
267
+ signal: requestAbortController.signal
268
+ });
269
+
270
+ if (!response.ok) {
271
+ if (response.status === 429) {
272
+ console.log("Rate limit hit, retrying in 1 second...");
273
+ await new Promise(resolve => setTimeout(resolve, 1000));
274
+ await sendQueryToAI(query);
275
+ return;
276
+ }
277
+ throw new Error(`Network response was not ok: ${response.status}`);
278
+ }
279
+
280
+ console.log("Streaming audio response received");
281
+ await handleStreamingResponse(response.body, voiceSelectionDropdown.value, requestAbortController.signal);
282
+ } catch (error) {
283
+ if (error.name !== 'AbortError') {
284
+ console.error("Error sending query to AI:", error);
285
+ }
286
+ } finally {
287
+ isRequestInProgress = false;
288
+ updateActivityIndicators();
289
+ }
290
+ };
291
+
292
+ // Handle the streaming audio response
293
+ const handleStreamingResponse = async (responseStream, voice, abortSignal) => {
294
+ const reader = responseStream.getReader();
295
+ const decoder = new TextDecoder("utf-8");
296
+ let buffer = "";
297
+ let initialChunksSent = 0;
298
+ let fullResponseText = "";
299
+ let textChunk = "";
300
+ let sentText = "";
301
+
302
+ try {
303
+ while (true) {
304
+ const { done, value } = await reader.read();
305
+ if (done) break;
306
+ if (abortSignal.aborted) throw new DOMException('Request aborted', 'AbortError');
307
+
308
+ if (isUserSpeaking) {
309
+ interruptAudioPlayback('user is speaking');
310
+ break;
311
+ }
312
+
313
+ const chunk = decoder.decode(value, { stream: true });
314
+ buffer += chunk;
315
+ const lines = buffer.split('\n');
316
+
317
+ for (let i = 0; i < lines.length - 1; i++) {
318
+ const line = lines[i];
319
+ if (line.startsWith('data: ')) {
320
+ const textContent = line.substring(6).trim();
321
+ if (textContent) {
322
+ if (!firstResponseTextTimestamp) firstResponseTextTimestamp = Date.now();
323
+
324
+ fullResponseText += textContent + " ";
325
+ textChunk += textContent + " ";
326
+ transcriptDiv.textContent = fullResponseText; // Update transcriptDiv
327
+
328
+ if (initialChunksSent < 2) {
329
+ const audioUrl = await generateTextToSpeechAudio(textContent, voice);
330
+ if (audioUrl) {
331
+ audioPlaybackQueue.push({ url: audioUrl, isPrefetched: false });
332
+ if (!currentAudio) playNextAudio();
333
+ }
334
+ sentText += textContent + " ";
335
+ initialChunksSent++;
336
+ } else {
337
+ let unsentTextChunk = textChunk.replace(sentText, '').trim();
338
+
339
+ if (unsentTextChunk.length >= CHUNK_SIZE) {
340
+ const audioUrl = await generateTextToSpeechAudio(unsentTextChunk, voice);
341
+ if (audioUrl) {
342
+ audioPlaybackQueue.push({ url: audioUrl, isPrefetched: false });
343
+ if (!currentAudio) playNextAudio();
344
+ }
345
+ textChunk = "";
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ buffer = lines[lines.length - 1];
353
+ }
354
+ } catch (error) {
355
+ console.error("Error in handleStreamingResponse:", error);
356
+ } finally {
357
+ reader.releaseLock();
358
+
359
+ let unsentTextChunk = textChunk.replace(sentText, '').trim();
360
+ if (unsentTextChunk !== "") {
361
+ const audioUrl = await generateTextToSpeechAudio(unsentTextChunk, voice);
362
+ if (audioUrl) {
363
+ audioPlaybackQueue.push({ url: audioUrl, isPrefetched: false });
364
+ if (!currentAudio) playNextAudio();
365
+ }
366
+ }
367
+
368
+ if (fullResponseText !== '') {
369
+ addToConversationHistory('assistant', fullResponseText);
370
+ fullResponseText = ''; // Clear fullResponseText for the next response
371
+ }
372
+ }
373
+ };
374
+
375
+ // Update activity indicators
376
+ const updateActivityIndicators = (state = null) => {
377
+ userActivityIndicator.textContent = isUserSpeaking ? "User: Speaking" : "User: Idle";
378
+ userActivityIndicator.className = isUserSpeaking
379
+ ? "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-blue-400 to-blue-600 hover:bg-gradient-to-r from-blue-500 to-blue-700"
380
+ : "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-gray-300 to-gray-400 dark:from-gray-700 dark:to-gray-800 hover:bg-gradient-to-r from-gray-400 to-gray-500"; // Tailwind classes
381
+
382
+ if (isRequestInProgress && !currentAudio) {
383
+ aiActivityIndicator.textContent = "AI: Processing...";
384
+ aiActivityIndicator.className = "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-purple-400 to-purple-600 hover:bg-gradient-to-r from-purple-500 to-purple-700"; // Tailwind class for thinking
385
+ } else if (currentAudio && !isUserSpeaking) {
386
+ aiActivityIndicator.textContent = state || "AI: Speaking";
387
+ aiActivityIndicator.className = "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-green-400 to-green-600 hover:bg-gradient-to-r from-green-500 to-green-700"; // Tailwind class for speaking
388
+ } else if (isUserSpeaking) {
389
+ aiActivityIndicator.textContent = "AI: Listening";
390
+ aiActivityIndicator.className = "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-yellow-400 to-yellow-600 hover:bg-gradient-to-r from-yellow-500 to-yellow-700"; // Tailwind class for listening
391
+ } else {
392
+ aiActivityIndicator.textContent = "AI: Idle";
393
+ aiActivityIndicator.className = "indicator rounded-full px-4 py-2 text-white flex items-center transition-colors duration-300 bg-gradient-to-r from-gray-300 to-gray-400 dark:from-gray-700 dark:to-gray-800 hover:bg-gradient-to-r from-gray-400 to-gray-500"; // Tailwind classes
394
+ }
395
+ };
396
+
397
+ // Initialize speech recognition
398
+ if ('webkitSpeechRecognition' in window) {
399
+ speechRecognizer = new webkitSpeechRecognition();
400
+ Object.assign(speechRecognizer, {
401
+ continuous: true,
402
+ interimResults: true,
403
+ language: 'en-US',
404
+ maxAlternatives: 3
405
+ });
406
+
407
+ speechRecognizer.onstart = () => {
408
+ console.log("Speech recognition started");
409
+ completeTranscript = '';
410
+ isUserSpeaking = true;
411
+ lastUserSpeechTimestamp = Date.now();
412
+ updateActivityIndicators();
413
+ startStopButton.innerHTML = `
414
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
415
+ <path d="M9 9h6v6h-6z"></path>
416
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
417
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
418
+ <line x1="12" y1="19" x2="12" y2="23"></line>
419
+ <line x1="8" y1="23" x2="16" y2="23"></line>
420
+ </svg>
421
+ Stop Listening
422
+ `;
423
+ };
424
+
425
+ speechRecognizer.onresult = (event) => {
426
+ let interimTranscript = '';
427
+ for (let i = event.resultIndex; i < event.results.length; i++) {
428
+ const transcript = event.results[i][0].transcript;
429
+ if (event.results[i].isFinal) {
430
+ completeTranscript += transcript;
431
+ interruptAudioPlayback('final');
432
+ processSpeechTranscript(completeTranscript);
433
+ completeTranscript = '';
434
+ isUserSpeaking = false;
435
+ updateActivityIndicators();
436
+ queryStartTime = Date.now();
437
+ } else {
438
+ interimTranscript += transcript;
439
+ isUserSpeaking = true;
440
+ lastUserSpeechTimestamp = Date.now();
441
+ updateActivityIndicators();
442
+
443
+ if (interimTranscript.length > prefetchTextQuery.length + 5) {
444
+ cancelPrefetchRequests(prefetchTextQuery);
445
+ }
446
+ prefetchTextQuery = interimTranscript;
447
+ prefetchFirstAudioChunk(interimTranscript, voiceSelectionDropdown.value);
448
+ }
449
+ }
450
+ };
451
+
452
+ speechRecognizer.onerror = (event) => {
453
+ console.error('Speech recognition error:', event.error);
454
+ if (isSpeechRecognitionActive) speechRecognizer.start();
455
+ };
456
+
457
+ speechRecognizer.onend = () => {
458
+ isUserSpeaking = false;
459
+ updateActivityIndicators();
460
+
461
+ if (!isRequestInProgress && completeTranscript !== '') {
462
+ processSpeechTranscript(completeTranscript);
463
+ completeTranscript = '';
464
+ }
465
+
466
+ if (isSpeechRecognitionActive) speechRecognizer.start();
467
+ };
468
+
469
+ startStopButton.addEventListener('click', () => {
470
+ if (isSpeechRecognitionActive && !isRequestInProgress) { // Stop Listening
471
+ speechRecognizer.stop();
472
+ isSpeechRecognitionActive = false;
473
+ startStopButton.innerHTML = `
474
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
475
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
476
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
477
+ <line x1="12" y1="19" x2="12" y2="23"></line>
478
+ <line x1="8" y1="23" x2="16" y2="23"></line>
479
+ </svg>
480
+ Start Listening
481
+ `;
482
+ } else if (isSpeechRecognitionActive && isRequestInProgress || currentAudio) { // Interrupt AI
483
+ interruptAudioPlayback('button interrupt');
484
+ speechRecognizer.start();
485
+ isSpeechRecognitionActive = true; // Keep recognition active
486
+ startStopButton.innerHTML = `
487
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
488
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
489
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
490
+ <line x1="12" y1="19" x2="12" y2="23"></line>
491
+ <line x1="8" y1="23" x2="16" y2="23"></line>
492
+ </svg>
493
+ Interrupt AI
494
+ `; // Replace with your SVG
495
+ } else { // Start Listening
496
+ speechRecognizer.start();
497
+ isSpeechRecognitionActive = true;
498
+ startStopButton.innerHTML = `
499
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
500
+ <path d="M9 9h6v6h-6z"></path>
501
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
502
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
503
+ <line x1="12" y1="19" x2="12" y2="23"></line>
504
+ <line x1="8" y1="23" x2="16" y2="23"></line>
505
+ </svg>
506
+ Stop Listening
507
+ `; // Replace with your SVG
508
+ }
509
+ });
510
+ } else {
511
+ alert('Your browser does not support the Web Speech API.');
512
+ }
513
+
514
+ // Add to conversation history
515
+ const addToConversationHistory = (role, content) => {
516
+ if (conversationHistory.length > 0 &&
517
+ conversationHistory[conversationHistory.length - 1].role === 'assistant' &&
518
+ conversationHistory[conversationHistory.length - 1].content === "") {
519
+ conversationHistory.pop();
520
+ }
521
+
522
+ conversationHistory.push({ role, content });
523
+
524
+ if (conversationHistory.length > 6) conversationHistory.splice(0, 2);
525
+ };
526
+
527
+ // Process the final speech transcript
528
+ const processSpeechTranscript = (transcript) => {
529
+ const trimmedTranscript = transcript.trimStart();
530
+ if (trimmedTranscript !== '' && !isRequestInProgress) {
531
+ activeQuery = trimmedTranscript;
532
+ sendQueryToAI(activeQuery);
533
+ addToConversationHistory('user', activeQuery);
534
+ transcriptDiv.textContent = '';
535
+ }
536
+ };
537
+
538
+ // Interrupt audio playback
539
+ const interruptAudioPlayback = (reason = 'unknown') => {
540
+ console.log(`Interrupting audio (reason: ${reason})...`);
541
+ if (currentAudio) {
542
+ currentAudio.pause();
543
+ currentAudio.currentTime = 0;
544
+ currentAudio = null;
545
+ }
546
+
547
+ audioPlaybackQueue.length = 0;
548
+ isRequestInProgress = false;
549
+
550
+ if (requestAbortController) {
551
+ requestAbortController.abort();
552
+ requestAbortController = null;
553
+ }
554
+
555
+ prefetchCache.clear();
556
+ prefetchQueue.length = 0;
557
+ updateActivityIndicators();
558
+ };
559
+
560
+ // Cancel pending prefetch requests
561
+ const cancelPrefetchRequests = (query) => {
562
+ const normalizedQuery = normalizeQueryText(query);
563
+
564
+ for (const [cacheKey, abortController] of pendingPrefetchRequests) {
565
+ if (cacheKey.startsWith(normalizedQuery)) {
566
+ abortController.abort();
567
+ pendingPrefetchRequests.delete(cacheKey);
568
+ }
569
+ }
570
+ };
571
+
572
+ // Update latency display
573
+ const updateLatency = () => {
574
+ if (firstResponseTextTimestamp) {
575
+ const latency = firstResponseTextTimestamp - queryStartTime;
576
+ responseTimeDisplay.textContent = `Latency: ${latency}ms`;
577
+ } else {
578
+ responseTimeDisplay.textContent = "Latency: 0ms";
579
+ }
580
+ };
581
+
582
+ setInterval(updateLatency, 200);