mirror of
https://github.com/multipleof4/devsune.git
synced 2026-01-14 08:27:55 +00:00
This build was committed by a bot.
This commit is contained in:
@@ -123,12 +123,8 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
/*
|
|
||||||
WARNING — SW STREAM BRIDGE CONTRACT
|
|
||||||
Purpose: allow chat streaming to continue while the page is backgrounded and seamlessly resume rendering when it becomes visible again. This page talks to a custom service worker using a BroadcastChannel named "chat-stream-v1". Protocol messages are: hello→hello-ack, start({id,payload}), delta({id,data}), done({id}), error({id,hint,aborted}), stop({id}), replay({id,offset}). If you rename the channel or change these message shapes, you must update sw.js to match; otherwise background-resume will break and streams may fail when the tab is hidden.
|
|
||||||
*/
|
|
||||||
const SWBRIDGE_CHANNEL='chat-stream-v1';
|
const SWBRIDGE_CHANNEL='chat-stream-v1';
|
||||||
const ChatStreamSW=(()=>{let bc=null,supports=false,inflight=new Map(),ensuring=null;const onmsg=e=>{const m=e.data||{};if(m.t==='hello-ack')supports=true;else if(m.t==='delta'||m.t==='done'||m.t==='error'){const f=inflight.get(m.id);if(!f)return;if(m.t==='delta'){f.full+=m.data||'';f.onDelta(m.data||'',false)}else if(m.t==='done'){f.onDelta('',true);f.res({ok:true,text:f.full});inflight.delete(m.id)}else{f.onDelta('\n\n'+(m.hint||'Request failed.'),true);f.res({ok:false,text:String(m.hint||''),aborted:!!m.aborted});inflight.delete(m.id)}}};const ensure=()=>ensuring||(ensuring=new Promise(r=>{if(!('serviceWorker'in navigator))return r(false);bc||(bc=new BroadcastChannel(SWBRIDGE_CHANNEL));bc.onmessage=onmsg;const poke=()=>{try{bc.postMessage({t:'hello',v:1})}catch{}};if(navigator.serviceWorker.controller){poke();setTimeout(()=>r(supports),150)}else navigator.serviceWorker.ready.then(()=>{poke();setTimeout(()=>r(supports),150)})}));const start=(payload,onDelta)=>{const id='cs-'+Date.now().toString(36)+Math.random().toString(36).slice(2,7);const p=new Promise(res=>inflight.set(id,{onDelta,full:'',res}));bc.postMessage({t:'start',id,payload});return{id,p}};const stop=id=>bc&&bc.postMessage({t:'stop',id});const replay=(id,offset)=>bc&&bc.postMessage({t:'replay',id,offset});document.addEventListener('visibilitychange',()=>{if(document.hidden)return;inflight.forEach((f,id)=>replay(id,f.full.length))});return{ensure,start,stop,replay,get ok(){return supports}}})();
|
const ChatStreamSW=(()=>{let bc=null,supports=false,inflight=new Map(),ensuring=null;const onmsg=e=>{const m=e.data||{};if(m.t==='hello-ack'){supports=true;alert('Service worker bridge connected (hello-ack) — background streaming available')}else if(m.t==='delta'||m.t==='done'||m.t==='error'){const f=inflight.get(m.id);if(!f)return;if(m.t==='delta'){f.full+=m.data||'';f.onDelta(m.data||'',false)}else if(m.t==='done'){f.onDelta('',true);f.res({ok:true,text:f.full});inflight.delete(m.id)}else{f.onDelta('\n\n'+(m.hint||'Request failed.'),true);f.res({ok:false,text:String(m.hint||''),aborted:!!m.aborted});inflight.delete(m.id)}}};const ensure=()=>ensuring||(ensuring=new Promise(r=>{if(!('serviceWorker'in navigator)){alert('No service worker support in this browser — background streaming unavailable');return r(false)}bc||(bc=new BroadcastChannel(SWBRIDGE_CHANNEL));bc.onmessage=onmsg;const poke=()=>{try{bc.postMessage({t:'hello',v:1})}catch{}};if(navigator.serviceWorker.controller){poke();setTimeout(()=>{if(!supports)alert('Service worker bridge not found — will use direct fetch fallback');r(supports)},150)}else navigator.serviceWorker.ready.then(()=>{poke();setTimeout(()=>{if(!supports)alert('Service worker bridge not found — will use direct fetch fallback');r(supports)},150)})}));const start=(payload,onDelta)=>{const id='cs-'+Date.now().toString(36)+Math.random().toString(36).slice(2,7);const p=new Promise(res=>inflight.set(id,{onDelta,full:'',res}));try{bc.postMessage({t:'start',id,payload});alert('Requested SW start for id '+id)}catch(e){alert('Failed to post start to SW for id '+id)};return{id,p}};const stop=id=>bc&&bc.postMessage({t:'stop',id});const replay=(id,offset)=>bc&&bc.postMessage({t:'replay',id,offset});document.addEventListener('visibilitychange',()=>{if(document.hidden)return;inflight.forEach((f,id)=>replay(id,f.full.length))});return{ensure,start,stop,replay,get ok(){return supports}}})();
|
||||||
const DEFAULT_MODEL='openai/gpt-5-chat',DEFAULT_API_KEY=''
|
const DEFAULT_MODEL='openai/gpt-5-chat',DEFAULT_API_KEY=''
|
||||||
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript','panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_presence_penalty','set_repetition_penalty','set_min_p','set_top_a','set_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput','scriptEditor','htmlEditor','subTabHTML','subTabJS','panelHTML','panelJS','suneHtml'].map(id=>[id,document.getElementById(id)]))
|
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript','panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_presence_penalty','set_repetition_penalty','set_min_p','set_top_a','set_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput','scriptEditor','htmlEditor','subTabHTML','subTabJS','panelHTML','panelJS','suneHtml'].map(id=>[id,document.getElementById(id)]))
|
||||||
const icons=()=>window.lucide&&lucide.createIcons()
|
const icons=()=>window.lucide&&lucide.createIcons()
|
||||||
@@ -161,7 +157,7 @@ const clearChat=()=>{state.messages=[];el.messages.innerHTML='';state.attachment
|
|||||||
const payloadWithSampling=b=>Object.assign({},b,{temperature:store.temperature,top_p:store.top_p,top_k:store.top_k,frequency_penalty:store.frequency_penalty,presence_penalty:store.presence_penalty,repetition_penalty:store.repetition_penalty,min_p:store.min_p,top_a:store.top_a})
|
const payloadWithSampling=b=>Object.assign({},b,{temperature:store.temperature,top_p:store.top_p,top_k:store.top_k,frequency_penalty:store.frequency_penalty,presence_penalty:store.presence_penalty,repetition_penalty:store.repetition_penalty,min_p:store.min_p,top_a:store.top_a})
|
||||||
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.()}}
|
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.()}}
|
||||||
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
|
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
|
||||||
async function askOpenRouterStreaming(onDelta){const apiKey=store.apiKey,model=store.model;if(!apiKey){const text=localDemoReply(state.messages[state.messages.length-1]?.content||'');onDelta(text,true);return {ok:true,text}}const msgs=[];if(store.system_prompt)msgs.push({role:'system',content:store.system_prompt});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.contentParts||m.content})));let body=payloadWithSampling({model,messages:msgs,stream:true});const re=store.reasoning_effort;if(re&&re!=='default')body.reasoning={effort:re};try{if(await ChatStreamSW.ensure()){const {id,p}=ChatStreamSW.start({url:'https://openrouter.ai/api/v1/chat/completions',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body},onDelta);state.controller={abort:()=>{ChatStreamSW.stop(id)}};const r=await p;return r}state.controller=new AbortController();const res=await fetch('https://openrouter.ai/api/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body:JSON.stringify(body),signal:state.controller.signal});if(!res.ok){const errText=await res.text().catch(()=> '');throw new Error(errText||('HTTP '+res.status))}const reader=res.body.getReader(),decoder=new TextDecoder('utf-8');let buffer='',full='',finished=false;const doneOnce=()=>{if(finished)return;finished=true;onDelta('',true)};while(true){const {value,done}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});let idx;while((idx=buffer.indexOf('\n\n'))!==-1){const chunk=buffer.slice(0,idx).trim();buffer=buffer.slice(idx+2);if(!chunk)continue;if(chunk.startsWith('data:')){const data=chunk.slice(5).trim();if(data==='[DONE]'){doneOnce();continue}try{const json=JSON.parse(data);const delta=json.choices?.[0]?.delta?.content??'';if(delta){full+=delta;onDelta(delta,false)}const finish=json.choices?.[0]?.finish_reason;if(finish)doneOnce()}catch{}}}}doneOnce();return {ok:true,text:full}}catch(e){const msg=String(e?.message||e),aborted=e?.name==='AbortError'||/abort/i.test(msg)||state.controller?.signal?.aborted||state.abortRequested;if(aborted){onDelta('',true);return {ok:false,text:'',aborted:true}}let hint='Request failed.';if(/401|unauthorized/i.test(msg))hint='Unauthorized (check API key).';else if(/429|rate/i.test(msg))hint='Rate limited (slow down or upgrade).';else if(/access|forbidden|403/i.test(msg))hint='Forbidden (model or key scope).';const fallback='\n\n'+hint;onDelta(fallback,true);return {ok:false,text:fallback}}finally{state.controller=null;state.abortRequested=false}}
|
async function askOpenRouterStreaming(onDelta){const apiKey=store.apiKey,model=store.model;if(!apiKey){const text=localDemoReply(state.messages[state.messages.length-1]?.content||'');onDelta(text,true);return {ok:true,text}};const msgs=[];if(store.system_prompt)msgs.push({role:'system',content:store.system_prompt});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.contentParts||m.content})));let body=payloadWithSampling({model,messages:msgs,stream:true});const re=store.reasoning_effort;if(re&&re!=='default')body.reasoning={effort:re};try{if(await ChatStreamSW.ensure()){const {id,p}=ChatStreamSW.start({url:'https://openrouter.ai/api/v1/chat/completions',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body},onDelta);state.controller={abort:()=>{ChatStreamSW.stop(id)}};const r=await p;return r}alert('No SW bridge available — using direct fetch streaming fallback');state.controller=new AbortController();const res=await fetch('https://openrouter.ai/api/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body:JSON.stringify(body),signal:state.controller.signal});if(!res.ok){const errText=await res.text().catch(()=> '');throw new Error(errText||('HTTP '+res.status))}const reader=res.body.getReader(),decoder=new TextDecoder('utf-8');let buffer='',full='',finished=false;const doneOnce=()=>{if(finished)return;finished=true;onDelta('',true)};while(true){const {value,done}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});let idx;while((idx=buffer.indexOf('\n\n'))!==-1){const chunk=buffer.slice(0,idx).trim();buffer=buffer.slice(idx+2);if(!chunk)continue;if(chunk.startsWith('data:')){const data=chunk.slice(5).trim();if(data==='[DONE]'){doneOnce();continue}try{const json=JSON.parse(data);const delta=json.choices?.[0]?.delta?.content??'';if(delta){full+=delta;onDelta(delta,false)}const finish=json.choices?.[0]?.finish_reason;if(finish)doneOnce()}catch{}}}}doneOnce();return {ok:true,text:full}}catch(e){const msg=String(e?.message||e),aborted=e?.name==='AbortError'||/abort/i.test(msg)||state.controller?.signal?.aborted||state.abortRequested;if(aborted){onDelta('',true);alert('Streaming aborted');return {ok:false,text:'',aborted:true}}let hint='Request failed.';if(/401|unauthorized/i.test(msg))hint='Unauthorized (check API key).';else if(/429|rate/i.test(msg))hint='Rate limited (slow down or upgrade).';else if(/access|forbidden|403/i.test(msg))hint='Forbidden (model or key scope).';const fallback='\n\n'+hint;onDelta(fallback,true);alert('Streaming request failed: '+hint);return {ok:false,text:fallback}}finally{state.controller=null;state.abortRequested=false}}
|
||||||
function localDemoReply(){return 'Tip: open the sidebar → Account & Backup to set your OpenRouter API key.'}
|
function localDemoReply(){return 'Tip: open the sidebar → Account & Backup to set your OpenRouter API key.'}
|
||||||
let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled'
|
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 TKEY='threads_v1',tload=()=>localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[]),tsave=v=>localforage.setItem(TKEY,v)
|
||||||
|
|||||||
Reference in New Issue
Block a user