diff --git a/index.html b/index.html index c04a1ea..69a04f2 100644 --- a/index.html +++ b/index.html @@ -178,7 +178,7 @@ let menuThreadId=null;const hideHistoryMenu=()=>{el.historyMenu.classList.add('h function showHistoryMenu(btn,id){menuThreadId=id;const r=btn.getBoundingClientRect();el.historyMenu.style.top=(r.bottom+4)+'px';el.historyMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.historyMenu.classList.remove('hidden');icons()} let menuSuneId=null;const hideSuneMenu=()=>{el.suneMenu.classList.add('hidden');menuSuneId=null} function showSuneMenu(btn,id){menuSuneId=id;const r=btn.getBoundingClientRect();el.suneMenu.style.top=(r.bottom+4)+'px';el.suneMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.suneMenu.classList.remove('hidden');icons()} -el.historyList.addEventListener('click',async e=>{const openBtn=e.target.closest('[data-open-thread]'),menuBtn=e.target.closest('[data-thread-menu]');if(openBtn){const id=openBtn.getAttribute('data-open-thread'),th=threads.find(t=>t.id===id);if(!th)return;state.currentThreadId=id;renderSuneHTML();clearChat();state.messages=Array.isArray(th.messages)?[...th.messages]:[];for(const m of state.messages){const b=msgRow(m);b.dataset.mid=m.id||'';renderMarkdown(b,partsToText(m.content))}queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden');hideHistoryMenu();return}if(menuBtn){e.stopPropagation();showHistoryMenu(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}}) +el.historyList.addEventListener('click',async e=>{const openBtn=e.target.closest('[data-open-thread]'),menuBtn=e.target.closest('[data-thread-menu]');if(openBtn){const id=openBtn.getAttribute('data-open-thread');if(id!==state.currentThreadId&&state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=false;state.controller=null}const th=threads.find(t=>t.id===id);if(!th)return;if(id===state.currentThreadId){el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden');hideHistoryMenu();return}state.currentThreadId=id;renderSuneHTML();clearChat();state.messages=Array.isArray(th.messages)?[...th.messages]:[];for(const m of state.messages){const b=msgRow(m);b.dataset.mid=m.id||'';renderMarkdown(b,partsToText(m.content))}syncWhileBusy();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden');hideHistoryMenu();return}if(menuBtn){e.stopPropagation();showHistoryMenu(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}}) el.historyMenu.addEventListener('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=threads.find(t=>t.id===menuThreadId);if(!th)return;if(act==='pin'){th.pinned=!th.pinned}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now()}}else if(act==='delete'){if(confirm('Delete this chat?')){threads=threads.filter(x=>x.id!==th.id);if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=Array.isArray(th.messages)?th.messages:[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m.content||'')||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}hideHistoryMenu();await tsave(threads);renderHistory()}) el.suneList.addEventListener('click',e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSuneMenu(menuBtn,menuBtn.getAttribute('[data-sune-menu]')?menuBtn.getAttribute('[data-sune-menu]'):menuBtn.getAttribute('data-sune-menu'));return}const btn=e.target.closest('[data-sune-id]');if(!btn)return;const id=btn.getAttribute('data-sune-id');if(id){su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')}}) el.suneMenu.addEventListener('click',e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=sunes.find(x=>x.id===menuSuneId);if(!s)return;if(act==='pin')s.pinned=!s.pinned;else if(act==='rename'){const nv=prompt('Rename sune to:',s.name);if(nv!=null)s.name=nv.trim()}else if(act==='pfp'){const url=prompt('Image URL:',s.avatar||'');if(url!==null)s.avatar=url.trim()}s.updatedAt=Date.now();su.save(sunes);hideSuneMenu();renderSidebar();reflectActiveSune()}) @@ -227,7 +227,7 @@ window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()}) init() const HTTP_BASE='https://orp.awww.workers.dev/ws' const buildBody=()=>{const msgs=[];if(store.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:store.masterPrompt}]});if(store.system_prompt)msgs.push({role:'system',content:[{type:'text',text:store.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:store.model,messages:msgs,stream:true});if(store.reasoning_effort&&store.reasoning_effort!=='default')b.reasoning={effort:store.reasoning_effort};if(store.verbosity)b.verbosity=store.verbosity;return b} -async function askOpenRouterStreaming(onDelta,streamId){if(!store.apiKey){const t=localDemoReply();onDelta(t,true);return {ok:true,rid:streamId||null}}const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null};await cacheStore.setItem(r.rid,'busy');const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const ws=new WebSocket(HTTP_BASE.replace('https','wss')+'?uid='+encodeURIComponent(r.rid));r.ws=ws;ws.onopen=()=>ws.send(JSON.stringify({type:'begin',rid:r.rid,provider:store.provider,apiKey:store.apiKey,or_body:buildBody()}));ws.onmessage=e=>{let m;try{m=JSON.parse(e.data)}catch{return}if(m.type==='delta'&&typeof m.seq==='number'&&m.seq>r.seq){r.seq=m.seq;onDelta(m.text||'',false)}else if(m.type==='done'||m.type==='err'){r.done=true;cacheStore.setItem(r.rid,'done');signal(m.type==='err'?'\n\n'+(m.message||'error'):'');ws.close()}};ws.onclose=()=>{};ws.onerror=()=>{};state.controller={abort:()=>{r.done=true;cacheStore.setItem(r.rid,'done');try{if(ws.readyState===1)ws.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{};signal('')}};return {ok:true,rid:r.rid}} +async function askOpenRouterStreaming(onDelta,streamId){if(!store.apiKey){const t=localDemoReply();onDelta(t,true);return {ok:true,rid:streamId||null}}const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null};await cacheStore.setItem(r.rid,'busy');const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const ws=new WebSocket(HTTP_BASE.replace('https','wss')+'?uid='+encodeURIComponent(r.rid));r.ws=ws;ws.onopen=()=>ws.send(JSON.stringify({type:'begin',rid:r.rid,provider:store.provider,apiKey:store.apiKey,or_body:buildBody()}));ws.onmessage=e=>{let m;try{m=JSON.parse(e.data)}catch{return}if(m.type==='delta'&&typeof m.seq==='number'&&m.seq>r.seq){r.seq=m.seq;onDelta(m.text||'',false)}else if(m.type==='done'||m.type==='err'){r.done=true;cacheStore.setItem(r.rid,'done');signal(m.type==='err'?'\n\n'+(m.message||'error'):'');ws.close()}};ws.onclose=()=>{};ws.onerror=()=>{};state.controller={abort:()=>{r.done=true;cacheStore.setItem(r.rid,'done');try{if(ws.readyState===1)ws.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{};signal('')},disconnect:()=>ws.close()};return {ok:true,rid:r.rid}} function openAccountSettings(){el.set_provider.value=store.provider||'openrouter';el.set_api_key_or.value=store.apiKeyOR||'';el.set_api_key_oai.value=store.apiKeyOAI||'';el.set_master_prompt.value=store.masterPrompt||'';el.accountSettingsModal.classList.remove('hidden')} function closeAccountSettings(){el.accountSettingsModal.classList.add('hidden')} el.accountSettingsOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()}) @@ -237,9 +237,9 @@ el.accountSettingsModal.addEventListener('click',e=>{if(e.target===el.accountSet el.accountSettingsForm.addEventListener('submit',e=>{e.preventDefault();store.provider=el.set_provider.value||'openrouter';store.apiKeyOR=String(el.set_api_key_or.value||'').trim();store.apiKeyOAI=String(el.set_api_key_oai.value||'').trim();store.masterPrompt=String(el.set_master_prompt.value||'').trim();closeAccountSettings()}) const lastAssistantId=()=>{const a=[...el.messages.querySelectorAll('.msg-bubble')];for(let i=a.length-1;i>=0;i--){const b=a[i],h=b.previousElementSibling;if(!h)continue;const you=/^\s*You\b/.test(h.textContent||'');if(!you)return b.dataset.mid||null}return null} const getBubbleById=id=>el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`) -async function syncActiveThread(){const id=lastAssistantId();if(!id)return false;if(await cacheStore.getItem(id)==='done'){if(state.busy){setBtnSend();state.busy=false}return false}const bubble=getBubbleById(id);if(!bubble)return false;const j=await(fetch(HTTP_BASE+'?uid='+encodeURIComponent(id)).then(r=>r.ok?r.json():null).catch(()=>null));const finalise=(t,c)=>{renderMarkdown(bubble,t,{enhance:false});enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(x=>x.id===id);if(i>=0)state.messages[i].content=c;else state.messages.push({id,role:'assistant',content:c,...activeMeta()});persistThread();setBtnSend();state.busy=false;cacheStore.setItem(id,'done')};if(!j||j.rid!==id){if(j&&j.error){const t=(bubble.textContent||'')+'\n\n'+j.error;finalise(t,[{type:'text',text:t}])}return false}const text=j.text||'',isDone=j.error||j.done||j.phase==='done';renderMarkdown(bubble,text,{enhance:false});if(isDone){finalise(text,[{type:'text',text}]);return false}await cacheStore.setItem(id,'busy');return true} +async function syncActiveThread(){const id=lastAssistantId();if(!id)return false;if(await cacheStore.getItem(id)==='done'){if(state.busy){setBtnSend();state.busy=false;state.controller=null}return false}if(!state.busy){state.busy=true;state.controller={abort:()=>{const ws=new WebSocket(HTTP_BASE.replace('https','wss'));ws.onopen=function(){this.send(JSON.stringify({type:'stop',rid:id}));this.close()}}};setBtnStop()}const bubble=getBubbleById(id);if(!bubble)return false;const j=await(fetch(HTTP_BASE+'?uid='+encodeURIComponent(id)).then(r=>r.ok?r.json():null).catch(()=>null));const finalise=(t,c)=>{renderMarkdown(bubble,t,{enhance:false});enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(x=>x.id===id);if(i>=0)state.messages[i].content=c;else state.messages.push({id,role:'assistant',content:c,...activeMeta()});persistThread();setBtnSend();state.busy=false;cacheStore.setItem(id,'done');state.controller=null};if(!j||j.rid!==id){if(j&&j.error){const t=(bubble.textContent||'')+'\n\n'+j.error;finalise(t,[{type:'text',text:t}])}return false}const text=j.text||'',isDone=j.error||j.done||j.phase==='done';renderMarkdown(bubble,text,{enhance:false});if(isDone){finalise(text,[{type:'text',text}]);return false}await cacheStore.setItem(id,'busy');return true} let syncLoopRunning=false -async function syncWhileBusy(){if(!state.busy||syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=true;try{while(state.busy&&await syncActiveThread())await new Promise(r=>setTimeout(r,1200))}finally{syncLoopRunning=false}} +async function syncWhileBusy(){if(syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=true;try{while(await syncActiveThread())await new Promise(r=>setTimeout(r,1200))}finally{syncLoopRunning=false}} ;['focus','pageshow'].forEach(ev=>window.addEventListener(ev,syncWhileBusy)) document.addEventListener('visibilitychange',()=>document.visibilityState==='visible'&&syncWhileBusy()) el.copySystemPrompt.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}})