mirror of
https://github.com/multipleof4/devsune.git
synced 2026-01-14 00:17:56 +00:00
Update index.html
This commit is contained in:
12
index.html
12
index.html
@@ -108,12 +108,10 @@
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
||||
<div id="badge" class="fixed top-3 right-3 z-50 px-2 py-1 rounded-full bg-gray-900 text-white text-xs shadow hidden">🧠 Reasoning…</div>
|
||||
<script>
|
||||
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','panelModel','panelPrompt','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_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','threadsDedupOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','badge'].map(id=>[id,document.getElementById(id)]))
|
||||
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','panelModel','panelPrompt','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_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','threadsDedupOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer'].map(id=>[id,document.getElementById(id)]))
|
||||
const clamp=(v,min,max)=>Math.max(min,Math.min(max,v)),num=(v,d)=>v==null||v===''||isNaN(+v)?d:+v,int=(v,d)=>v==null||v===''||isNaN(parseInt(v))?d:parseInt(v),gid=()=>Math.random().toString(36).slice(2,9),esc=s=>String(s).replace(/[&<>'"`]/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'","`":"`"}[c]))
|
||||
const showReason=()=>el.badge.classList.remove('hidden'),hideReason=()=>el.badge.classList.add('hidden')
|
||||
const globalStore={get apiKey(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKey(v){localStorage.setItem('openrouter_api_key',v||'')}}
|
||||
const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}}
|
||||
const defaultSettings={model:DEFAULT_MODEL,temperature:1,top_p:0.97,top_k:0,frequency_penalty:0,presence_penalty:0,repetition_penalty:1,min_p:0,top_a:0,system_prompt:''}
|
||||
@@ -130,7 +128,7 @@ const renderSidebar=()=>{const list=[...sunes].sort((a,b)=>(b.pinned-a.pinned));
|
||||
function enhanceCodeBlocks(root,doHL=true){root.querySelectorAll('pre>code').forEach(code=>{if(code.textContent.length>200000)return;const pre=code.parentElement;pre.classList.add('relative','rounded-xl','border','border-gray-200');if(!pre.querySelector('.copy-btn')){const btn=document.createElement('button');btn.className='copy-btn';btn.textContent='Copy';btn.addEventListener('click',async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(code.innerText);btn.textContent='Copied';setTimeout(()=>btn.textContent='Copy',1200)}catch{}});pre.appendChild(btn)}if(doHL&&window.hljs&&code.textContent.length<100000)hljs.highlightElement(code)})}
|
||||
const md=window.markdownit({html:false,linkify:true,typographer:true,breaks:true})
|
||||
const getSuneLabel=m=>{const name=(m&&m.sune_name)||getActiveSune().name,modelShort=getModelShort(m&&m.model);return `${name} · ${modelShort}`}
|
||||
function msgRow(m){const role=typeof m==='string'?m:(m&&m.role)||'assistant';const meta=typeof m==='string'?{}:m||{};const row=document.createElement('div');row.className='flex flex-col gap-2';const head=document.createElement('div');head.className='flex items-center gap-2 px-4';const avatar=document.createElement('div');if(role==='user'){avatar.className='bg-gray-900 text-white msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center';avatar.textContent='🧑'}else{if(meta&&meta.avatar){avatar.className='msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden';const img=document.createElement('img');img.src=meta.avatar;img.className='h-full w-full object-cover';avatar.appendChild(img)}else{avatar.className='bg-gray-200 text-gray-900 msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center';avatar.textContent='✺'}}const name=document.createElement('div');name.className='text-xs font-medium text-gray-500';name.textContent=role==='user'?'You':getSuneLabel(meta);head.appendChild(avatar);head.appendChild(name);const bubble=document.createElement('div');bubble.className=(role==='user'?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full';row.appendChild(head);row.appendChild(bubble);el.messages.appendChild(row);queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));return bubble}
|
||||
function msgRow(m){const role=typeof m==='string'?m:(m&&m.role)||'assistant';const meta=typeof m==='string'?{}:m||{};const row=document.createElement('div');row.className='flex flex-col gap-2';const head=document.createElement('div');head.className='flex items-center gap-2 px-4';const avatar=document.createElement('div');if(role==='user'){avatar.className='bg-gray-900 text-white msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center';avatar.textContent='🧑'}else{if(meta&&meta.avatar){avatar.className='msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden';const img=document.createElement('img');img.src=meta.avatar;img.className='h-full w-full object-cover';avatar.appendChild(img)}else{avatar.className='bg-gray-200 text-gray-900 msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center';avatar.textContent='✺'}}const name=document.createElement('div');name.className='text-xs font-medium text-gray-500';name.textContent=role==='user'?'You':getSuneLabel(meta);head.appendChild(avatar);head.appendChild(name);let rtag=null;if(role!=='user'){rtag=document.createElement('span');rtag.className='ml-2 inline-flex items-center gap-1 rounded-full px-2 py-[2px] text-[10px] font-medium bg-gray-900 text-white hidden';rtag.textContent='🧠 Reasoning';head.appendChild(rtag)}const bubble=document.createElement('div');bubble.className=(role==='user'?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full';row.appendChild(head);row.appendChild(bubble);if(role!=='user')bubble._badge=rtag;el.messages.appendChild(row);queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));return bubble}
|
||||
function renderMarkdown(node,text,opt={enhance:true,highlight:true}){node.innerHTML=md.render(text);if(opt.enhance)enhanceCodeBlocks(node,opt.highlight)}
|
||||
function addMessage(m,track=true){const bubble=msgRow(m);renderMarkdown(bubble,m.content);if(track)state.messages.push(m);return bubble}
|
||||
const addSuneBubbleStreaming=meta=>msgRow(Object.assign({role:'assistant'},meta))
|
||||
@@ -138,7 +136,7 @@ const clearChat=()=>{state.messages=[];el.messages.innerHTML=''}
|
||||
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='<svg viewBox="0 0 24 24" class="h-5 w-5" fill="currentColor"><rect x="7" y="7" width="10" height="10" rx="1"/></svg>';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='<svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7"/></svg>';b.onclick=null}
|
||||
async function askOpenRouterStreaming(onDelta){const apiKey=store.apiKey,model=store.model;hideReason();if(!apiKey){const text=localDemoReply(state.messages[state.messages.length-1]?.content||'');onDelta(text,true);return {ok:true,text}}try{state.controller=new AbortController();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.content})));const body=payloadWithSampling({model,messages:msgs,stream:true});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,sawR=false,sawC=false;const doneOnce=()=>{if(finished)return;finished=true;hideReason();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),d=json.choices?.[0]?.delta||{},delta=d.content??'';if(d.reasoning!==undefined&&!sawR){sawR=true;showReason()}if(delta){if(sawR&&!sawC){sawC=true;hideReason()}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){hideReason();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;hideReason();onDelta(fallback,true);return {ok:false,text:fallback}}finally{state.controller=null;state.abortRequested=false}}
|
||||
async function askOpenRouterStreaming(onDelta,badge){const apiKey=store.apiKey,model=store.model;if(badge)badge.classList.add('hidden');if(!apiKey){const text=localDemoReply(state.messages[state.messages.length-1]?.content||'');onDelta(text,true);return {ok:true,text}}try{state.controller=new AbortController();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.content})));const body=payloadWithSampling({model,messages:msgs,stream:true});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,sawR=false,sawC=false;const doneOnce=()=>{if(finished)return;finished=true;if(badge)badge.classList.add('hidden');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),d=json.choices?.[0]?.delta||{},delta=d.content??'';if(d.reasoning!==undefined&&!sawR){sawR=true;if(badge)badge.classList.remove('hidden')}if(delta){if(sawR&&!sawC){sawC=true;if(badge)badge.classList.add('hidden')}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){if(badge)badge.classList.add('hidden');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;if(badge)badge.classList.add('hidden');onDelta(fallback,true);return {ok:false,text:fallback}}finally{state.controller=null;state.abortRequested=false}}
|
||||
function localDemoReply(prompt){const tips=['Tip: open the sidebar → Account & Backup to set your OpenRouter API key.','Click 🤖 to change model & sampling.','New chats are stateless here—no history is kept.'],tip=tips[Math.floor(Math.random()*tips.length)],mirrored=prompt.split(/\s+/).slice(0,24).join(' ');return `Local demo mode. You said: "${mirrored}"\n\n${tip}`}
|
||||
const idb={db:null,open(){return new Promise((res,rej)=>{const r=indexedDB.open('chat_history_v1',1);r.onupgradeneeded=()=>{r.result.createObjectStore('threads',{keyPath:'id'})};r.onsuccess=()=>{this.db=r.result;res()};r.onerror=()=>rej(r.error)})},all(){return new Promise((res,rej)=>{const tx=this.db.transaction('threads').objectStore('threads').getAll();tx.onsuccess=()=>res(tx.result||[]);tx.onerror=()=>rej(tx.error)})},get(id){return new Promise((res,rej)=>{const tx=this.db.transaction('threads').objectStore('threads').get(id);tx.onsuccess=()=>res(tx.result||null);tx.onerror=()=>rej(tx.error)})},put(v){return new Promise((res,rej)=>{const tx=this.db.transaction('threads','readwrite').objectStore('threads').put(v);tx.onsuccess=()=>res();tx.onerror=()=>rej(tx.error)})},del(id){return new Promise((res,rej)=>{const tx=this.db.transaction('threads','readwrite').objectStore('threads').delete(id);tx.onsuccess=()=>res();tx.onerror=()=>rej(tx.error)})}}
|
||||
let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled'
|
||||
@@ -158,7 +156,7 @@ const raf=(fn=>{let id=null;return()=>{if(id)cancelAnimationFrame(id);id=request
|
||||
const big=()=>el.input.value.length>5000||el.input.value.split('\n').length>200
|
||||
const fit=()=>{const large=big();el.input.style.overflowY=large?'auto':'hidden';el.input.style.height='auto';el.input.style.height=(large?160:Math.min(el.input.scrollHeight,160))+'px'}
|
||||
const fitRAF=raf(fit)
|
||||
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text);el.input.value='';fit();addMessage({role:'user',content:text});state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);let buf='',completed=false;await askOpenRouterStreaming((delta,done)=>{buf+=delta;renderMarkdown(suneBubble,buf,{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push({role:'assistant',content:buf,...suneMeta});persistThread();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}})})
|
||||
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text);el.input.value='';fit();addMessage({role:'user',content:text});state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);let buf='',completed=false;await askOpenRouterStreaming((delta,done)=>{$1}, suneBubble._badge);if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push({role:'assistant',content:buf,...suneMeta});persistThread();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}})})
|
||||
el.input.addEventListener('input',fitRAF)
|
||||
el.input.addEventListener('paste',()=>setTimeout(fit,0))
|
||||
function openSettings(){const a=getActiveSune(),s=a.settings;el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_presence_penalty.value=s.presence_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_system_prompt.value=s.system_prompt;showModelTab();el.settingsModal.classList.remove('hidden')}
|
||||
@@ -191,4 +189,4 @@ window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()})
|
||||
init()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user