Update index.html

This commit is contained in:
2025-08-25 13:28:09 -07:00
committed by GitHub
parent 12e761ac12
commit 574f1df4f4

View File

@@ -46,7 +46,6 @@
<div class="p-3 border-t relative">
<button id="userMenuBtn" class="w-full flex items-center justify-between px-3 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition" hx-on="click:event.stopPropagation();document.getElementById('userMenu').classList.toggle('hidden')"><span class="flex items-center gap-2"><span class="h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center">👤</span><span class="text-sm">Account & Backup</span></span><i data-lucide="chevron-down" class="h-4 w-4"></i></button>
<div id="userMenu" class="absolute left-3 right-3 bottom-16 translate-y-2 rounded-xl border border-gray-200 bg-white shadow-lg hidden overflow-hidden">
<button id="accountSettingsOption" class="menu-item"><i data-lucide="settings" class="h-4 w-4"></i><span>Settings</span></button>
<button id="apiKeyOption" class="menu-item">Enter OpenRouter API key</button>
<button id="sunesImportOption" class="menu-item">Import sunes (.json)</button>
<button id="sunesExportOption" class="menu-item">Export sunes (.json)</button>
@@ -64,6 +63,7 @@
<div id="historyMenu" class="menu-card hidden">
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
<button data-action="sync" class="menu-item hidden"><i data-lucide="refresh-ccw" class="h-4 w-4"></i><span>Sync</span></button>
<button data-action="delete" class="menu-item text-red-600"><i data-lucide="trash-2" class="h-4 w-4"></i><span>Delete</span></button>
<button data-action="count_tokens" class="menu-item"><i data-lucide="hash" class="h-4 w-4"></i><span>Count tokens (approx.)</span></button>
</div>
@@ -94,16 +94,17 @@
<div><label class="block text-gray-700 font-medium mb-1">Verbosity</label><select id="set_verbosity" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select><p class="mt-1 text-xs text-gray-500">OpenAI param.</p></div>
</div>
</div>
<div id="panelPrompt" class="p-4 space-y-4 hidden">
<div><label class="block text-gray-700 font-medium mb-1">System Prompt</label><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Enter a system prompt to guide the sune"></textarea><p class="mt-1 text-xs text-gray-500">Saved per sune.</p></div>
<div id="panelPrompt" class="p-4 space-y-2 hidden">
<div><label class="block text-gray-700 font-medium mb-1">System Prompt</label><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Enter a system prompt to guide the sune"></textarea></div>
<div class="flex gap-2"><button type="button" id="copySys" class="px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteSys" class="px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200">Paste</button></div>
</div>
<div id="panelScript" class="p-4 space-y-3 hidden">
<div class="grid grid-cols-2 gap-2 text-xs font-medium">
<button type="button" id="subTabHTML" class="py-2 px-3 rounded-xl bg-black text-white border border-gray-200">HTML</button>
<button type="button" id="subTabJS" class="py-2 px-3 rounded-xl bg-gray-100 border border-gray-200">JavaScript</button>
</div>
<div id="panelHTML" class=""><pre id="htmlEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5" contenteditable="true" spellcheck="false"></pre><p class="mt-1 text-xs text-gray-500">Scripts also run.</p></div>
<div id="panelJS" class="hidden"><pre id="scriptEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5" contenteditable="plaintext-only" spellcheck="false"></pre><p class="mt-1 text-xs text-gray-500">Put scripts in HTML instead for now. This does nothing.</p></div>
<div id="panelHTML" class=""><pre id="htmlEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5" contenteditable="plaintext-only" spellcheck="false"></pre><div class="mt-2 flex gap-2"><button type="button" id="copyHTML" class="px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteHTML" class="px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200">Paste</button></div></div>
<div id="panelJS" class="hidden"><pre id="scriptEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5" contenteditable="plaintext-only" spellcheck="false"></pre><div class="mt-2 flex gap-2"><button type="button" id="copyJS" class="px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteJS" class="px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200">Paste</button></div></div>
</div>
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
<button type="button" id="deleteSuneBtn" class="inline-flex items-center gap-2 px-3 py-2 rounded-xl border border-red-200 text-red-700 hover:bg-red-50"><svg viewBox="0 0 24 24" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6v12a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V6m-9 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span>Delete sune</span></button>
@@ -113,38 +114,26 @@
</div>
</div>
</div>
<div id="accountSettingsModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div>
<div class="absolute inset-x-0 top-16 mx-auto w-full max-w-md px-4">
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Account Settings</span><button id="closeAccountSettings" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
<form id="accountSettingsForm" class="p-4 text-sm space-y-4">
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div class="flex items-center justify-end gap-2 pt-2 border-t"><button type="button" id="cancelAccountSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
</form>
</div>
</div>
</div>
<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>
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
<script>
const DEFAULT_MODEL='openai/gpt-5',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_max_tokens','set_verbosity','set_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','accountSettingsOption','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput','scriptEditor','htmlEditor','subTabHTML','subTabJS','panelHTML','panelJS','suneHtml','accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','set_master_prompt'].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_max_tokens','set_verbosity','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','copySys','pasteSys','copyHTML','pasteHTML','copyJS','pasteJS'].map(id=>[id,document.getElementById(id)]))
const icons=()=>window.lucide&&lucide.createIcons()
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=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;","`":"&#96;"}[c]))
const fmtSize=b=>{const u=['B','KB','MB','GB','TB'];let i=0,x=b;while(x>=1024&&i<u.length-1){x/=1024;i++}return (x>=10?Math.round(x):Math.round(x*10)/10)+' '+u[i]}
const asDataURL=f=>new Promise(r=>{const fr=new FileReader();fr.onload=()=>r(String(fr.result||''));fr.readAsDataURL(f)})
const b64=x=>x.split(',')[1]||''
const globalStore={get apiKey(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKey(v){localStorage.setItem('openrouter_api_key',v||'')},get masterPrompt(){return localStorage.getItem('master_prompt')||''},set masterPrompt(v){localStorage.setItem('master_prompt',v||'')}}
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,max_tokens:0,verbosity:'',reasoning_effort:'default',system_prompt:'',script:'',html:''}
const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{})})
let sunes=(su.load()||[]).map(makeSune)
if(!sunes.length){const def=makeSune({name:'Default'});sunes=[def];su.save(sunes);su.setActiveId(def.id)}
const getActiveSune=()=>sunes.find(a=>a.id===su.getActiveId())||sunes[0],createDefaultSune=()=>makeSune({name:'Default'})
const store=new Proxy({},{get(_,p){if(p==='apiKey')return globalStore.apiKey;if(p==='masterPrompt')return globalStore.masterPrompt;const a=getActiveSune();if(p==='model')return a.settings.model;if(p in a.settings)return a.settings[p];if(p==='system_prompt')return a.settings.system_prompt},set(_,p,v){if(p==='apiKey'){globalStore.apiKey=v;return true}if(p==='masterPrompt'){globalStore.masterPrompt=v;return true}const i=sunes.findIndex(a=>a.id===getActiveSune().id);if(i>=0){if(p==='model')sunes[i].settings.model=v||DEFAULT_MODEL;else if(p==='system_prompt')sunes[i].settings.system_prompt=v||'';else sunes[i].settings[p]=v;sunes[i].updatedAt=Date.now();su.save(sunes);return true}return false}})
const store=new Proxy({},{get(_,p){const a=getActiveSune();if(p==='apiKey')return globalStore.apiKey;if(p==='model')return a.settings.model;if(p in a.settings)return a.settings[p];if(p==='system_prompt')return a.settings.system_prompt},set(_,p,v){if(p==='apiKey'){globalStore.apiKey=v;return true}const i=sunes.findIndex(a=>a.id===getActiveSune().id);if(i>=0){if(p==='model')sunes[i].settings.model=v||DEFAULT_MODEL;else if(p==='system_prompt')sunes[i].settings.system_prompt=v||'';else sunes[i].settings[p]=v;sunes[i].updatedAt=Date.now();su.save(sunes);return true}return false}})
const state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:false}}
const getModelShort=m=>{const mm=m||store.model||'';return mm.includes('/')?mm.split('/').pop():mm}
const renderSuneHTML=()=>{const m=el.suneHtml,h=(getActiveSune().settings.html||'').trim();m.innerHTML='';m.classList.toggle('hidden',!h);if(!h)return;m.insertAdjacentHTML('afterbegin',h);m.querySelectorAll('script').forEach(s=>{const n=document.createElement('script');[...s.attributes].forEach(a=>n.setAttribute(a.name,a.value));n.text=s.text; s.replaceWith(n)})}
@@ -171,11 +160,11 @@ async function persistThread(){if(!state.currentThreadId)return;let th=threads.f
const historyRow=t=>`<div class=\"relative flex items-center gap-2 px-3 py-2 ${t.pinned?'bg-yellow-50':''}\"><button data-open-thread=\"${t.id}\" class=\"flex-1 text-left truncate\">${t.pinned?'📌 ':''}${esc(t.title)}</button><button data-thread-menu=\"${t.id}\" class=\"h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center\" title=\"More\"><i data-lucide=\"more-horizontal\" class=\"h-4 w-4\"></i></button></div>`
async function renderHistory(){const list=[...threads].sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt));el.historyList.innerHTML=list.map(historyRow).join('');icons()}
let menuThreadId=null;const hideHistoryMenu=()=>{el.historyMenu.classList.add('hidden');menuThreadId=null}
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()}
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');el.historyMenu.querySelector('[data-action="sync"]').classList.toggle('hidden',id!==state.currentThreadId);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.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.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==='sync'){const last=(()=>{for(let i=state.messages.length-1;i>=0;i--){const m=state.messages[i];if(m&&m.role==='assistant'&&m.id)return m.id}return null})(),b=last?document.querySelector(`.msg-bubble[data-mid="${last}"]`):null;if(!last||!b){hideHistoryMenu();return}const j=await fetchTranscript(last);if(j&&j.text){renderMarkdown(b,j.text,{enhance:false});if(j.done||j.phase==='done'||j.error){const m=state.messages.find(x=>x.id===last);if(m){m.content=[{type:'text',text:j.text+(j.error?'\n\n'+j.error:'')}];await persistThread()}enhanceCodeBlocks(b,true)}}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()})
function updateAttachBadge(){const n=state.attachments.length;el.attachBadge.textContent=String(n);el.attachBadge.classList.toggle('hidden',n===0)}
@@ -185,7 +174,7 @@ function addAttachmentTree(role,arr){if(!arr?.length)return;const id=gid(),text=
el.attachBtn.addEventListener('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.value=''};el.fileInput.click()})
el.fileInput.addEventListener('change',async()=>{const files=[...(el.fileInput.files||[])];if(!files.length)return;for(const f of files){const at=await toAttach(f).catch(()=>null);if(at)state.attachments.push(at)}updateAttachBadge()})
el.messages.addEventListener('click',async e=>{const a=e.target.closest('a[href^="#dl-"]');if(!a)return; e.preventDefault();const m=a.getAttribute('href').match(/^#dl-([^-]+)-(\d+)$/);if(!m)return;const id=m[1],i=+m[2];const msg=state.messages.find(x=>x.id===id),meta=msg?.attachmentsMeta?.[i];if(!meta)return;let blob;if(meta.mode==='dataURL'){blob=await (await fetch(meta.data)).blob()}else{const bin=Uint8Array.from(atob(meta.data),c=>c.charCodeAt(0));blob=new Blob([bin],{type:meta.mime||'application/octet-stream'})}const url=URL.createObjectURL(blob),dl=document.createElement('a');dl.href=url;dl.download=meta.name||'download';document.body.appendChild(dl);dl.click();dl.remove();URL.revokeObjectURL(url)})
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text||'(attachments)');el.input.value='';const parts=[];if(text)parts.push({type:'text',text});state.attachments.forEach(a=>parts.push(a.part));addMessage({role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]});if(state.attachments.length)addAttachmentTree('user',state.attachments);state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);state.stream={rid:null,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done)=>{buf+=delta;state.stream.text=buf;renderMarkdown(suneBubble,buf,{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push(Object.assign({role:'assistant',content:[{type:'text',text:buf}]},suneMeta));persistThread();state.stream={rid:null,bubble:null,meta:null,text:'',done:false};queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}};const res=await askOpenRouterStreaming(onDelta);state.stream.rid=res?.rid||null;state.attachments=[];updateAttachBadge()})
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text||'(attachments)');el.input.value='';const parts=[];if(text)parts.push({type:'text',text});state.attachments.forEach(a=>parts.push(a.part));addMessage({role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]});if(state.attachments.length)addAttachmentTree('user',state.attachments);state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);const rid=(crypto.randomUUID?crypto.randomUUID():gid());suneBubble.dataset.mid=rid;state.stream={rid,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done)=>{buf+=delta;state.stream.text=buf;renderMarkdown(suneBubble,buf,{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push(Object.assign({id:rid,role:'assistant',content:[{type:'text',text:buf}]},suneMeta));persistThread();state.stream={rid:null,bubble:null,meta:null,text:'',done:false};queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}};await askOpenRouterStreaming(onDelta,rid);state.attachments=[];updateAttachBadge()})
let jars={js:null,html:null};const ensureJars=async()=>{if(jars.js&&jars.html)return jars;const mod=await import('https://medv.io/codejar/codejar.js');const CodeJar=mod.CodeJar||mod.default;const mk=(elx,lang)=>CodeJar(elx,ed=>{ed.innerHTML=hljs.highlight(ed.textContent,{language:lang}).value},{tab:' '});if(!jars.js)jars.js=mk(el.scriptEditor,'javascript');if(!jars.html)jars.html=mk(el.htmlEditor,'xml');return jars}
let openedJS=false,openedHTML=false
function openSettings(){const a=getActiveSune(),s=a.settings;openedJS=false;openedHTML=false;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_max_tokens.value=s.max_tokens||'';el.set_verbosity.value=s.verbosity||'';el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;showTab('Model');el.settingsModal.classList.remove('hidden')}
@@ -202,6 +191,12 @@ el.tabPrompt.addEventListener('click',()=>showTab('Prompt'))
el.tabScript.addEventListener('click',()=>showTab('Script'))
el.subTabHTML.addEventListener('click',()=>showScriptSubTab('HTML'))
el.subTabJS.addEventListener('click',()=>showScriptSubTab('JS'))
el.copySys.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}})
el.pasteSys.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{}})
el.pasteHTML.addEventListener('click',async()=>{try{const t=await navigator.clipboard.readText();(await ensureJars()).html.updateCode(t)}catch{}})
el.copyJS.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.scriptEditor.textContent||'')}catch{}})
el.pasteJS.addEventListener('click',async()=>{try{const t=await navigator.clipboard.readText();(await ensureJars()).js.updateCode(t)}catch{}})
el.settingsForm.addEventListener('submit',e=>{e.preventDefault();const a=getActiveSune(),s=a.settings;s.model=(el.set_model.value||DEFAULT_MODEL).trim();s.temperature=clamp(num(el.set_temperature.value,1.0),0,2);s.top_p=clamp(num(el.set_top_p.value,1.0),0,1);s.top_k=Math.max(0,int(el.set_top_k.value,0));s.frequency_penalty=clamp(num(el.set_frequency_penalty.value,0.0),-2,2);s.presence_penalty=clamp(num(el.set_presence_penalty.value,0.0),-2,2);s.repetition_penalty=clamp(num(el.set_repetition_penalty.value,1.0),0,2);s.min_p=clamp(num(el.set_min_p.value,0.0),0,1);s.top_a=clamp(num(el.set_top_a.value,0.0),0,1);s.max_tokens=Math.max(0,int(el.set_max_tokens.value,0));s.verbosity=(el.set_verbosity.value||'');s.reasoning_effort=(el.set_reasoning_effort.value||'default');s.system_prompt=el.set_system_prompt.value.trim();const oldScript=s.script,oldHtml=s.html;const edScript=el.scriptEditor?.textContent||'',edHtml=el.htmlEditor?.textContent||'';s.script=openedJS?edScript:oldScript;s.html=openedHTML?edHtml:oldHtml;a.updatedAt=Date.now();su.save(sunes);closeSettings();reflectActiveSune()})
el.deleteSuneBtn.addEventListener('click',()=>{const activeId=su.getActiveId(),active=getActiveSune(),name=active?.name||'this sune';if(!confirm(`Delete "${name}"?`))return;sunes=sunes.filter(a=>a.id!==activeId);su.save(sunes);if(sunes.length===0){const def=createDefaultSune();sunes=[def];su.save(sunes);su.setActiveId(def.id)}else{su.setActiveId(sunes[0].id)}renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
el.newSuneBtn.addEventListener('click',()=>{const name=prompt('Name your sune:');if(!name)return;const id=gid();sunes.unshift({id,name:name.trim(),pinned:false,avatar:'',updatedAt:Date.now(),settings:Object.assign({},defaultSettings)});su.save(sunes);su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')})
@@ -223,20 +218,13 @@ async function init(){threads=await tload();await renderHistory();renderSidebar(
window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()})
init()
const WS_BASE='wss://orp.awww.workers.dev/ws'
const WS_UID=(crypto&&crypto.randomUUID?crypto.randomUUID():Math.random().toString(36).slice(2))
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){if(!store.apiKey){const t=localDemoReply();onDelta(t,true);return {ok:true,rid:null}}const r={rid:Math.random().toString(36).slice(2),seq:-1,done:false,manual:false,signaled:false,backoff:300,ws:null};const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const open=mode=>{if(r.done)return;r.ws=new WebSocket(WS_BASE+'?uid='+encodeURIComponent(WS_UID));r.ws.onopen=()=>r.ws.send(JSON.stringify(mode==='begin'?{type:'begin',rid:r.rid,apiKey:store.apiKey,or_body:buildBody()}:{type:'resume',rid:r.rid,after:r.seq}));r.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('');r.ws.close()}else if(m.type==='err'){r.done=true;signal('\n\n'+(m.message||'error'));r.ws.close()}};r.ws.onclose=()=>{if(!r.done&&!r.manual)setTimeout(()=>open('resume'),r.backoff=Math.min(r.backoff*1.5,5000))};r.ws.onerror=()=>{}};state.controller={abort:()=>{r.manual=true;r.done=true;try{r.ws?.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{}try{r.ws?.close()}catch{}signal('')}};open('begin');return {ok:true,rid:r.rid}}
function openAccountSettings(){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()})
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.masterPrompt=String(el.set_master_prompt.value||'').trim();closeAccountSettings()})
async function fetchTranscript(){try{const r=await fetch(WS_BASE+'?uid='+encodeURIComponent(WS_UID));if(!r.ok)return null;return await r.json()}catch{return null}}
async function syncTranscript(){const s=state.stream;if(!s||!s.bubble||!s.rid)return;const j=await fetchTranscript();if(!j||!j.rid||j.rid!==s.rid)return;const t=j.text||'';if(t&&t!==s.text){s.text=t;renderMarkdown(s.bubble,t,{enhance:false})}if((j.done||j.phase==='done')&&!s.done){s.done=true;setBtnSend();state.busy=false;enhanceCodeBlocks(s.bubble,true);state.messages.push(Object.assign({role:'assistant',content:[{type:'text',text:s.text}]},s.meta));persistThread();state.stream={rid:null,bubble:null,meta:null,text:'',done:false};queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}if(j.error&&!s.done){s.done=true;setBtnSend();state.busy=false;s.text=(s.text||'')+'\n\n'+String(j.error||'');renderMarkdown(s.bubble,s.text,{enhance:false});enhanceCodeBlocks(s.bubble,true);state.messages.push(Object.assign({role:'assistant',content:[{type:'text',text:s.text}]},s.meta));persistThread();state.stream={rid:null,bubble:null,meta:null,text:'',done:false};queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}}
const buildBody=()=>{const msgs=[];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,uid){if(!store.apiKey){const t=localDemoReply();onDelta(t,true);return {ok:true}}const r={rid:uid,seq:-1,done:false,manual:false,signaled:false,backoff:300,ws:null};const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const open=mode=>{if(r.done)return;r.ws=new WebSocket(WS_BASE+'?uid='+encodeURIComponent(uid));r.ws.onopen=()=>r.ws.send(JSON.stringify(mode==='begin'?{type:'begin',rid:r.rid,apiKey:store.apiKey,or_body:buildBody()}:{type:'resume',rid:r.rid,after:r.seq}));r.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('');r.ws.close()}else if(m.type==='err'){r.done=true;signal('\n\n'+(m.message||'error'));r.ws.close()}};r.ws.onclose=()=>{if(!r.done&&!r.manual)setTimeout(()=>open('resume'),r.backoff=Math.min(r.backoff*1.5,5000))};r.ws.onerror=()=>{}};state.controller={abort:()=>{r.manual=true;r.done=true;try{r.ws?.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{}try{r.ws?.close()}catch{}signal('')}};open('begin');return {ok:true}}
async function fetchTranscript(uid){try{const r=await fetch(WS_BASE+'?uid='+encodeURIComponent(uid));if(!r.ok)return null;return await r.json()}catch{return null}}
async function syncTranscript(id){const s=state.stream;if(!id&&(!s||!s.bubble||!s.rid))return;const rid=id||s.rid,j=await fetchTranscript(rid);if(!j||!j.rid||j.rid!==rid)return;const t=j.text||'';if(!id){if(t&&t!==s.text){s.text=t;renderMarkdown(s.bubble,t,{enhance:false})}if((j.done||j.phase==='done')&&!s.done){s.done=true;setBtnSend();state.busy=false;enhanceCodeBlocks(s.bubble,true);state.messages.push(Object.assign({id:rid,role:'assistant',content:[{type:'text',text:s.text}]},s.meta));persistThread();state.stream={rid:null,bubble:null,meta:null,text:'',done:false};queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}if(j.error&&!s.done){s.done=true;setBtnSend();state.busy=false;s.text=(s.text||'')+'\n\n'+String(j.error||'');renderMarkdown(s.bubble,s.text,{enhance:false});enhanceCodeBlocks(s.bubble,true);state.messages.push(Object.assign({id:rid,role:'assistant',content:[{type:'text',text:s.text}]},s.meta));persistThread();state.stream={rid:null,bubble:null,meta:null,text:'',done:false};queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}}
window.addEventListener('focus',()=>syncTranscript())
document.addEventListener('visibilitychange',()=>{if(!document.hidden)syncTranscript()})
</script>
</body>
</html>
``` [end:stop]