diff --git a/index.html b/index.html index a0d1325..c04a1ea 100644 --- a/index.html +++ b/index.html @@ -169,6 +169,7 @@ function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b function localDemoReply(){return 'Tip: open the sidebar → Account & Backup to set your API key.'} let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled' const TKEY='threads_v1',tload=()=>localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[]),tsave=v=>localforage.setItem(TKEY,v) +const cacheStore=localforage.createInstance({name:'master_cache'}); async function ensureThreadOnFirstUser(text){let needNew=!state.currentThreadId;if(state.messages.length===0)state.currentThreadId=null;if(state.currentThreadId&&!threads.some(x=>x.id===state.currentThreadId))needNew=true;if(!needNew)return;const id=gid(),now=Date.now(),th={id,title:titleFrom(text),pinned:false,updatedAt:now,messages:[]};state.currentThreadId=id;threads.unshift(th);await tsave(threads);await renderHistory()} async function persistThread(){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId);if(!th)return;th.messages=[...state.messages];th.updatedAt=Date.now();th.title=titleFrom(partsToText(th.messages.find(m=>m.role==='user')?.content)||th.title);await tsave(threads);await renderHistory()} const historyRow=t=>`
` @@ -224,9 +225,9 @@ window.USER={log:async s=>{const t=String(s??'').trim();if(!t)return;await ensur async function init(){threads=await tload();await renderHistory();renderSidebar();reflectActiveSune();clearChat();icons();kbBind();kbUpdate()} window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()}) init() -const WS_BASE='wss://orp.awww.workers.dev/ws' +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};const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const ws=new WebSocket(WS_BASE+'?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'){r.done=true;signal('');ws.close()}else if(m.type==='err'){r.done=true;signal('\n\n'+(m.message||'error'));ws.close()}};ws.onclose=()=>{};ws.onerror=()=>{};state.controller={abort:()=>{r.done=true;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('')}};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()}) @@ -234,16 +235,13 @@ el.closeAccountSettings.addEventListener('click',closeAccountSettings) el.cancelAccountSettings.addEventListener('click',closeAccountSettings) el.accountSettingsModal.addEventListener('click',e=>{if(e.target===el.accountSettingsModal||e.target.classList.contains('bg-black/30'))closeAccountSettings()}) 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 HTTP_BASE = "https://orp.awww.workers.dev/ws" -async function fetchTranscript(id){try{const r=await fetch(HTTP_BASE+'?uid='+encodeURIComponent(id));if(!r.ok)return null;return await r.json()}catch{return null}} 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 syncTranscript(){const id=lastAssistantId();if(!id)return false;const bubble=getBubbleById(id);if(!bubble)return false;const j=await fetchTranscript(id).catch(()=>null);if(!j||j.rid!==id){if(j&&j.error){setBtnSend();state.busy=false;const t=(bubble.textContent||'')+'\n\n'+j.error;renderMarkdown(bubble,t,{enhance:false});enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread()}return false}const t=j.text||'';renderMarkdown(bubble,t,{enhance:false});if(j.error||j.done||j.phase==='done'){setBtnSend();state.busy=false;enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread();return false}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}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} let syncLoopRunning=false -const syncKick=r=>{if(!state.busy||document.visibilityState==='hidden')return;setTimeout(()=>syncLoop(r),0)} -async function syncLoop(){if(!state.busy||syncLoopRunning)return;const id=lastAssistantId();if(!id)return;syncLoopRunning=true;try{for(;;){if(!state.busy)break;const cont=await syncTranscript().catch(()=>false);if(!cont)break;await new Promise(r=>setTimeout(r,1200))}}finally{syncLoopRunning=false}} -;['focus','pageshow'].forEach(ev=>window.addEventListener(ev,()=>syncKick(ev))) -document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')syncKick('visible')}) +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}} +;['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{}}) el.pasteSystemPrompt.addEventListener('click',async()=>{try{el.set_system_prompt.value=await navigator.clipboard.readText()}catch{}}) el.copyHTML.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.htmlEditor.textContent||'')}catch{}})