This build was committed by a bot.

This commit is contained in:
github-actions
2025-09-03 12:54:45 +00:00
parent 73428dce8b
commit d4715fa58a
3 changed files with 24 additions and 21 deletions

36
dist/index.html vendored
View File

@@ -23,7 +23,7 @@
<style>html,body{overscroll-behavior-y:contain}</style>
<style></style>
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body class="bg-white text-gray-900 selection:bg-black/10" hx-on="click: if(!document.getElementById('threadPopover').contains(event.target)&&!event.target.closest('[data-thread-menu]')) hideThreadPopover(); if(!document.getElementById('sunePopover').contains(event.target)&&!event.target.closest('[data-sune-menu]')) hideSunePopover(); if(!document.getElementById('userMenu').contains(event.target)&&!document.getElementById('userMenuBtn').contains(event.target)) document.getElementById('userMenu').classList.add('hidden')">
<body class="bg-white text-gray-900 selection:bg-black/10" hx-on="click: if(event.target.closest('button')) haptic(); if(!document.getElementById('threadPopover').contains(event.target)&&!event.target.closest('[data-thread-menu]')) hideThreadPopover(); if(!document.getElementById('sunePopover').contains(event.target)&&!event.target.closest('[data-sune-menu]')) hideSunePopover(); if(!document.getElementById('userMenu').contains(event.target)&&!document.getElementById('userMenuBtn').contains(event.target)) document.getElementById('userMenu').classList.add('hidden')">
<div class="flex flex-col h-dvh max-h-dvh">
<header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
@@ -55,7 +55,7 @@
<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="sunesImportOption" class="menu-item">Import sunes (.json)</button>
<button id="sunesExportOption" class="menu-item">Export sunes (.json)</button>
<button id="sunesExportOption" class="menu-item">Export sunes (.sune)</button>
<button id="threadsImportOption" class="menu-item">Import threads (.json)</button>
<button id="threadsExportOption" class="menu-item">Export threads (.json)</button>
</div>
@@ -77,7 +77,7 @@
<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="pfp" class="menu-item"><i data-lucide="image" class="h-4 w-4"></i><span>Change pfp</span></button>
<button data-action="export" class="menu-item"><i data-lucide="download" class="h-4 w-4"></i><span>Export sune (.json)</span></button>
<button data-action="export" class="menu-item"><i data-lucide="download" class="h-4 w-4"></i><span>Export sune (.sune)</span></button>
</div>
<div id="suneModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div>
@@ -131,7 +131,7 @@
<div id="accountPanelGeneral" class="p-4 space-y-4">
<div><label class="block text-gray-700 font-medium mb-1">Provider</label><select id="set_provider" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="openrouter">OpenRouter</option><option value="openai">OpenAI</option></select><p class="mt-1 text-xs text-gray-500">Or you can just prefix names with or: or oai: to override.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" 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><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it" value="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
</div>
<div id="accountPanelAPI" class="p-4 space-y-4 hidden">
<div><label class="block text-gray-700 font-medium mb-1">OpenRouter API Key</label><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-or-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
@@ -155,6 +155,7 @@
const DEFAULT_MODEL='openai/gpt-5',DEFAULT_API_KEY=''
const el=Object.fromEntries(['topbar','chat','messages','composer','input','sendBtn','suneBtnTop','suneModal','suneURL','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','sidebarLeft','sidebarOverlayLeft','sidebarBtnLeft','suneList','newSuneBtn','userMenuBtn','userMenu','accountSettingsOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','sidebarBtnRight','sidebarRight','sidebarOverlayRight','threadList','closeThreads','threadPopover','sunePopover','footer','attachBtn','attachBadge','fileInput','htmlEditor','extensionHtmlEditor','htmlTab_index','htmlTab_extension','suneHtml','accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','set_master_prompt','set_provider','set_api_key_or','set_api_key_oai','set_title_model','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','accountTabGeneral','accountTabAPI','accountPanelGeneral','accountPanelAPI','set_gh_token','importAccountSettings','exportAccountSettings','importAccountSettingsInput'].map(id=>[id,document.getElementById(id)]))
const icons=()=>window.lucide&&lucide.createIcons()
const haptic=()=>/android/i.test(navigator.userAgent)&&navigator.vibrate?.(4)
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 sid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6)
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]}
@@ -169,8 +170,10 @@ const SUNE=window.SUNE=new Proxy({get list(){return sunes},get id(){return su.ge
if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)}
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||SUNE.model||'';return mm.includes('/')?mm.split('/').pop():mm}
const renderSuneHTML=()=>{const c=[SUNE.html,SUNE.extension_html].map(x=>(x||'').trim()).join('\n'),m=el.suneHtml;m.innerHTML='';m.classList.toggle('hidden',!c);if(!c)return;m.insertAdjacentHTML('afterbegin',c);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)})}
const reflectActiveSune=()=>{const a=SUNE.active;el.suneBtnTop.title=`Settings — ${a.name}`;el.suneBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺';icons();renderSuneHTML()}
const resolveSuneSrc=src=>{if(!src)return null;if(src.startsWith('gh://')){const path=src.substring(5),parts=path.split('/');if(parts.length<3)return null;const[owner,repo,...filePathParts]=parts;return`https://raw.githubusercontent.com/${owner}/${repo}/main/${filePathParts.join('/')}`}return src}
const processSuneIncludes=async(html,depth=0)=>{if(depth>5)return'<!-- Sune include depth limit reached -->';if(!html)return'';const includeRegex=/<sune\s+src=['"]([^'"]+)['"]\s*\/>/gi;const matches=[...html.matchAll(includeRegex)];if(matches.length===0)return html;const replacements=await Promise.all(matches.map(async match=>{const[tag,rawSrc]=match;const fetchUrl=resolveSuneSrc(rawSrc);if(!fetchUrl)return{tag,content:`<!-- Sune Include Error: Invalid src "${esc(rawSrc)}" -->`};try{const response=await fetch(fetchUrl);if(!response.ok)throw new Error(`HTTP ${response.status}`);const suneData=await response.json();const suneObj=Array.isArray(suneData)?suneData[0]:suneData;const combinedHtml=[suneObj?.settings?.html||'',suneObj?.settings?.extension_html||''].join('\n');const processedContent=await processSuneIncludes(combinedHtml,depth+1);return{tag,content:processedContent}}catch(error){return{tag,content:`<!-- Sune Include Error: Failed to fetch/parse ${esc(fetchUrl)}. ${esc(error.message)} -->`}}}));let resultHtml=html;for(const{tag,content}of replacements){resultHtml=resultHtml.replace(tag,content)}return resultHtml}
const renderSuneHTML=async()=>{const baseHtml=[SUNE.html,SUNE.extension_html].map(x=>(x||'').trim()).join('\n');const finalHtml=await processSuneIncludes(baseHtml);const container=el.suneHtml;container.innerHTML='';container.classList.toggle('hidden',!finalHtml.trim());if(!finalHtml.trim())return;container.insertAdjacentHTML('afterbegin',finalHtml);container.querySelectorAll('script').forEach(s=>{if(s.type&&s.type!=='text/javascript')return;const newScript=document.createElement('script');[...s.attributes].forEach(a=>newScript.setAttribute(a.name,a.value));newScript.text=s.text;s.replaceWith(newScript)})}
const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.title=`Settings — ${a.name}`;el.suneBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺';await renderSuneHTML();icons()}
const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===SUNE.id?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-6 w-6 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.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>`
const renderSidebar=()=>{const list=[...SUNE.list].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()}
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)})}
@@ -197,10 +200,10 @@ let menuThreadId=null;const hideThreadPopover=()=>{el.threadPopover.classList.ad
function showThreadPopover(btn,id){menuThreadId=id;const r=btn.getBoundingClientRect();el.threadPopover.style.top=(r.bottom+4)+'px';el.threadPopover.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.threadPopover.classList.remove('hidden');icons()}
let menuSuneId=null;const hideSunePopover=()=>{el.sunePopover.classList.add('hidden');menuSuneId=null}
function showSunePopover(btn,id){menuSuneId=id;const r=btn.getBoundingClientRect();el.sunePopover.style.top=(r.bottom+4)+'px';el.sunePopover.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.sunePopover.classList.remove('hidden');icons()}
el.threadList.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.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();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.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();return}if(menuBtn){e.stopPropagation();showThreadPopover(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}})
el.threadList.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.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();return}state.currentThreadId=id;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))}await renderSuneHTML();syncWhileBusy();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();return}if(menuBtn){e.stopPropagation();showThreadPopover(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}})
el.threadPopover.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+')')}hideThreadPopover();await tsave(threads);renderThreads()})
el.suneList.addEventListener('click',e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSunePopover(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){if(state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=false;state.controller=null};SUNE.setActive(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')}})
el.sunePopover.addEventListener('click',e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=SUNE.get(menuSuneId);if(!s)return;const updateAndRender=()=>{s.updatedAt=Date.now();SUNE.save();renderSidebar();reflectActiveSune()};if(act==='pin'){s.pinned=!s.pinned;updateAndRender()}else if(act==='rename'){const n=prompt('Rename sune to:',s.name);if(n!=null){s.name=n.trim();updateAndRender()}}else if(act==='pfp'){const i=document.createElement('input');i.type='file';i.accept='image/*';i.onchange=()=>{const f=i.files?.[0];if(!f)return;const img=new Image;img.onload=()=>{const c=document.createElement('canvas'),ctx=c.getContext('2d'),D=144;let w=img.width,h=img.height;if(Math.max(w,h)>D)w>h?(h=D*h/w,w=D):(w=D*w/h,h=D);c.width=w;c.height=h;ctx.drawImage(img,0,0,w,h);s.avatar=c.toDataURL('image/webp',.84);updateAndRender();URL.revokeObjectURL(img.src)};img.src=URL.createObjectURL(f)};i.click()}else if(act==='export')dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${ts()}.json`,[s]);hideSunePopover()})
el.suneList.addEventListener('click',async e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSunePopover(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){if(state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=false;state.controller=null};SUNE.setActive(id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')}})
el.sunePopover.addEventListener('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=SUNE.get(menuSuneId);if(!s)return;const updateAndRender=async()=>{s.updatedAt=Date.now();SUNE.save();renderSidebar();await reflectActiveSune()};if(act==='pin'){s.pinned=!s.pinned;await updateAndRender()}else if(act==='rename'){const n=prompt('Rename sune to:',s.name);if(n!=null){s.name=n.trim();await updateAndRender()}}else if(act==='pfp'){const i=document.createElement('input');i.type='file';i.accept='image/*';i.onchange=()=>{const f=i.files?.[0];if(!f)return;const img=new Image;img.onload=async()=>{const c=document.createElement('canvas'),ctx=c.getContext('2d'),D=144;let w=img.width,h=img.height;if(Math.max(w,h)>D)w>h?(h=D*h/w,w=D):(w=D*w/h,h=D);c.width=w;c.height=h;ctx.drawImage(img,0,0,w,h);s.avatar=c.toDataURL('image/webp',.84);await updateAndRender();URL.revokeObjectURL(img.src)};img.src=URL.createObjectURL(f)};i.click()}else if(act==='export')dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${ts()}.sune`,[s]);hideSunePopover()})
function updateAttachBadge(){const n=state.attachments.length;el.attachBadge.textContent=String(n);el.attachBadge.classList.toggle('hidden',n===0)}
async function toAttach(file){if(!file)return null;const pick=(name,bytes,mime,data,mode,part)=>({name,bytes,mime,data,mode,part});if(file instanceof File){const name=file.name||'file',mime=(file.type||'application/octet-stream').toLowerCase(),bytes=file.size||0;if(/^image\//.test(mime)||/\.(png|jpe?g|webp|gif)$/i.test(name)){const data=await asDataURL(file);return pick(name,bytes,mime,data,'dataURL',{type:'image_url',image_url:{url:data}})}if(mime==='application/pdf'||/\.pdf$/i.test(name)){const data=await asDataURL(file),bin=b64(data);return pick(name.endsWith('.pdf')?name:name+'.pdf',bytes,'application/pdf',bin,'base64',{type:'file',file:{filename:name,file_data:bin}})}if(/^audio\//.test(mime)||/\.(wav|mp3)$/i.test(name)){const data=await asDataURL(file),bin=b64(data),fmt=/mp3/.test(mime)||/\.mp3$/i.test(name)?'mp3':'wav';return pick(name,bytes,mime,bin,'base64',{type:'input_audio',input_audio:{data:bin,format:fmt}})}const data=await asDataURL(file),bin=b64(data);return pick(name,bytes,mime,bin,'base64',{type:'file',file:{filename:name,file_data:bin}})}if(file&&file.name==null&&file.data){const name=file.name||'file',mime=(file.mime||'application/octet-stream').toLowerCase(),bytes=file.size||0;if(/^image\//.test(mime)){const url=`data:${mime};base64,${file.data}`;return pick(name,bytes,mime,url,'dataURL',{type:'image_url',image_url:{url}})}if(mime==='application/pdf'){return pick(name,bytes,mime,file.data,'base64',{type:'file',file:{filename:name,file_data:file.data}})}if(/^audio\//.test(mime)){const fmt=/mp3/.test(mime)?'mp3':'wav';return pick(name,bytes,mime,file.data,'base64',{type:'input_audio',input_audio:{data:file.data,format:fmt}})}return pick(name,bytes,mime,file.data,'base64',{type:'file',file:{filename:name,file_data:file.data}})}return null}
function attachmentsText(id,arr){const head='**Attachments**',list=arr.map((a,i)=>`- [${esc(a.name)}${fmtSize(a.bytes)}](#dl-${id}-${i})`).join('\n');return head+'\n'+list}
@@ -216,29 +219,28 @@ const closeSettings=()=>{el.suneModal.classList.add('hidden')}
const tabs={Model:['tabModel','panelModel'],Prompt:['tabPrompt','panelPrompt'],Script:['tabScript','panelScript']}
function showTab(key){Object.entries(tabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)});if(key==='Script'){openedHTML=true;showHtmlTab('index');ensureJars().then(({html,extension})=>{const s=SUNE.settings;html.updateCode(s.html||'');extension.updateCode(s.extension_html||'')})}}
el.suneBtnTop.addEventListener('click',openSettings)
el.closeSettings?.addEventListener('click',closeSettings)
el.cancelSettings.addEventListener('click',closeSettings)
el.suneModal.addEventListener('click',e=>{if(e.target===el.suneModal||e.target.classList.contains('bg-black/30'))closeSettings()})
el.tabModel.addEventListener('click',()=>showTab('Model'))
el.tabPrompt.addEventListener('click',()=>showTab('Prompt'))
el.tabScript.addEventListener('click',()=>showTab('Script'))
el.settingsForm.addEventListener('submit',e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||DEFAULT_MODEL).trim();SUNE.temperature=clamp(num(el.set_temperature.value,1.0),0,2);SUNE.top_p=clamp(num(el.set_top_p.value,1.0),0,1);SUNE.top_k=Math.max(0,int(el.set_top_k.value,0));SUNE.frequency_penalty=clamp(num(el.set_frequency_penalty.value,0.0),-2,2);SUNE.presence_penalty=clamp(num(el.set_presence_penalty.value,0.0),-2,2);SUNE.repetition_penalty=clamp(num(el.set_repetition_penalty.value,1.0),0,2);SUNE.min_p=clamp(num(el.set_min_p.value,0.0),0,1);SUNE.top_a=clamp(num(el.set_top_a.value,0.0),0,1);SUNE.max_tokens=Math.max(0,int(el.set_max_tokens.value,0));SUNE.verbosity=(el.set_verbosity.value||'');SUNE.reasoning_effort=(el.set_reasoning_effort.value||'default');SUNE.system_prompt=el.set_system_prompt.value.trim();if(openedHTML){SUNE.html=el.htmlEditor.textContent;SUNE.extension_html=el.extensionHtmlEditor.textContent}closeSettings();reflectActiveSune()})
el.deleteSuneBtn.addEventListener('click',()=>{const activeId=SUNE.id,name=SUNE.name||'this sune';if(!confirm(`Delete "${name}"?`))return;SUNE.delete(activeId);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
el.newSuneBtn.addEventListener('click',()=>{const name=prompt('Name your sune:');if(!name)return;const sune=SUNE.create({name:name.trim()});SUNE.setActive(sune.id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')})
function dl(name,obj){const blob=new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
el.settingsForm.addEventListener('submit',async e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||DEFAULT_MODEL).trim();SUNE.temperature=clamp(num(el.set_temperature.value,1.0),0,2);SUNE.top_p=clamp(num(el.set_top_p.value,1.0),0,1);SUNE.top_k=Math.max(0,int(el.set_top_k.value,0));SUNE.frequency_penalty=clamp(num(el.set_frequency_penalty.value,0.0),-2,2);SUNE.presence_penalty=clamp(num(el.set_presence_penalty.value,0.0),-2,2);SUNE.repetition_penalty=clamp(num(el.set_repetition_penalty.value,1.0),0,2);SUNE.min_p=clamp(num(el.set_min_p.value,0.0),0,1);SUNE.top_a=clamp(num(el.set_top_a.value,0.0),0,1);SUNE.max_tokens=Math.max(0,int(el.set_max_tokens.value,0));SUNE.verbosity=(el.set_verbosity.value||'');SUNE.reasoning_effort=(el.set_reasoning_effort.value||'default');SUNE.system_prompt=el.set_system_prompt.value.trim();if(openedHTML){SUNE.html=el.htmlEditor.textContent;SUNE.extension_html=el.extensionHtmlEditor.textContent}closeSettings();await reflectActiveSune()})
el.deleteSuneBtn.addEventListener('click',async()=>{const activeId=SUNE.id,name=SUNE.name||'this sune';if(!confirm(`Delete "${name}"?`))return;SUNE.delete(activeId);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
el.newSuneBtn.addEventListener('click',async()=>{const name=prompt('Name your sune:');if(!name)return;const sune=SUNE.create({name:name.trim()});SUNE.setActive(sune.id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')})
function dl(name,obj){const blob=new Blob([JSON.stringify(obj,null,2)],{type:name.endsWith('.sune')?'application/octet-stream':'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
const ts=()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`}
let importMode=null
el.sunesExportOption.addEventListener('click',()=>{dl(`sunes-${ts()}.json`,{version:1,sunes:SUNE.list,activeId:SUNE.id});el.userMenu.classList.add('hidden')})
el.sunesExportOption.addEventListener('click',()=>{dl(`sunes-${ts()}.sune`,{version:1,sunes:SUNE.list,activeId:SUNE.id});el.userMenu.classList.add('hidden')})
el.sunesImportOption.addEventListener('click',()=>{importMode='sunes';el.importInput.value='';el.importInput.click()})
el.threadsExportOption.addEventListener('click',()=>{dl(`threads-${ts()}.json`,{version:1,threads});el.userMenu.classList.add('hidden')})
el.threadsImportOption.addEventListener('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()})
el.importInput.addEventListener('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const text=await file.text();const data=JSON.parse(text);if(importMode==='sunes'){const list=Array.isArray(data)?data:(Array.isArray(data.sunes)?data.sunes:[]);if(!list.length)throw new Error('No sunes');const incoming=list.map(a=>makeSune(a||{}));const map={};incoming.forEach(s=>{if(!s.id)s.id=gid();const k=s.id,prev=map[k];map[k]=!prev||(+s.updatedAt>+prev.updatedAt)?s:prev});let added=0,updated=0;const idx=Object.fromEntries(sunes.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex){sunes.push(s);added++}else if(+s.updatedAt>+ex.updatedAt){Object.assign(ex,s);updated++}});SUNE.save();if(data.activeId&&sunes.some(x=>x.id===data.activeId))SUNE.setActive(data.activeId);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();alert(`${added} new, ${updated} updated.`)}else if(importMode==='threads'){const arr=Array.isArray(data)?data:(Array.isArray(data.threads)?data.threads:[]);if(!arr.length)throw new Error('No threads');const norm=t=>({id:t.id||gid(),title:titleFrom(t.title||titleFrom(t.messages?.find?.(m=>m.role==='user')?.content||'')),pinned:!!t.pinned,updatedAt:t.updatedAt||Date.now(),messages:Array.isArray(t.messages)?t.messages.filter(m=>m&&m.role&&m.content):[]});const best={};arr.forEach(t=>{const n=norm(t),k=n.id,prev=best[k];best[k]=!prev||(+n.updatedAt>+prev.updatedAt)?n:prev});let kept=0,skipped=0;const idx=Object.fromEntries(threads.map(t=>[t.id,t]));for(const th of Object.values(best)){const ex=idx[th.id];if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}if(!ex)threads.push(th);else Object.assign(ex,th);kept++}await tsave(threads);await renderThreads();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
el.importInput.addEventListener('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const text=await file.text();const data=JSON.parse(text);if(importMode==='sunes'){const list=Array.isArray(data)?data:(Array.isArray(data.sunes)?data.sunes:[]);if(!list.length)throw new Error('No sunes');const incoming=list.map(a=>makeSune(a||{}));const map={};incoming.forEach(s=>{if(!s.id)s.id=gid();const k=s.id,prev=map[k];map[k]=!prev||(+s.updatedAt>+prev.updatedAt)?s:prev});let added=0,updated=0;const idx=Object.fromEntries(sunes.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex){sunes.push(s);added++}else if(+s.updatedAt>+ex.updatedAt){Object.assign(ex,s);updated++}});SUNE.save();if(data.activeId&&sunes.some(x=>x.id===data.activeId))SUNE.setActive(data.activeId);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();alert(`${added} new, ${updated} updated.`)}else if(importMode==='threads'){const arr=Array.isArray(data)?data:(Array.isArray(data.threads)?data.threads:[]);if(!arr.length)throw new Error('No threads');const norm=t=>({id:t.id||gid(),title:titleFrom(t.title||titleFrom(t.messages?.find?.(m=>m.role==='user')?.content||'')),pinned:!!t.pinned,updatedAt:t.updatedAt||Date.now(),messages:Array.isArray(t.messages)?t.messages.filter(m=>m&&m.role&&m.content):[]});const best={};arr.forEach(t=>{const n=norm(t),k=n.id,prev=best[k];best[k]=!prev||(+n.updatedAt>+prev.updatedAt)?n:prev});let kept=0,skipped=0;const idx=Object.fromEntries(threads.map(t=>[t.id,t]));for(const th of Object.values(best)){const ex=idx[th.id];if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}if(!ex)threads.push(th);else Object.assign(ex,th);kept++}await tsave(threads);await renderThreads();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
function kbUpdate(){const vv=window.visualViewport;const overlap=vv?Math.max(0,(window.innerHeight-(vv.height+vv.offsetTop))):0;document.documentElement.style.setProperty('--kb',overlap+'px');const fh=el.footer.getBoundingClientRect().height;document.documentElement.style.setProperty('--footer-h',fh+'px');el.footer.style.transform='translateY('+(-overlap)+'px)';el.chat.style.scrollPaddingBottom=(fh+overlap+16)+'px'}
function kbBind(){if(window.visualViewport){['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,()=>kbUpdate(),{passive:true}))}['resize','orientationchange'].forEach(ev=>window.addEventListener(ev,()=>setTimeout(kbUpdate,50),{passive:true}));['focus','click'].forEach(ev=>el.input.addEventListener(ev,()=>{setTimeout(()=>{kbUpdate();el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)}))}
function activeMeta(){return {sune_name:SUNE.name,model:SUNE.model,avatar:SUNE.avatar}}
Object.assign(window.SUNE,{attach:async(files,opts={})=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser(clean[0]?.name||'(attachments)');const meta=activeMeta();const o=typeof opts==='boolean'?{toAPI:opts,tree:true}:(opts||{});const toAPI=('toAPI'in o)?!!o.toAPI:(('toapi'in o)?!!o.toapi:true);const tree=('tree'in o)?!!o.tree:true;if(toAPI){const parts=clean.map(a=>a.part);addMessage({role:'assistant',content:parts,...meta})}if(tree)addAttachmentTree('assistant',clean);await persistThread()},log:async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await persistThread()}})
window.USER={log:async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'user',content:[{type:'text',text:t}]});await persistThread()},get PAT(){return globalStore.ghToken}}
async function init(){threads=await tload();await renderThreads();renderSidebar();reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
async function init(){threads=await tload();await renderThreads();renderSidebar();await reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
window.addEventListener('resize',()=>{hideThreadPopover();hideSunePopover()})
const htmlTabs={index:['htmlTab_index','htmlEditor'],extension:['htmlTab_extension','extensionHtmlEditor']};function showHtmlTab(key){Object.entries(htmlTabs).forEach(([k,[tb,pn]])=>{const a=k===key;el[tb].classList.toggle('border-black',a);el[tb].classList.toggle('border-transparent',!a);el[tb].classList.toggle('hover:border-gray-300',!a);el[pn].classList.toggle('hidden',!a)})}
el.htmlTab_index.textContent='index.html';el.htmlTab_extension.textContent='extension.html';

2
dist/sw.js vendored
View File

@@ -1 +1 @@
if(!self.define){let e,i={};const t=(t,n)=>(t=new URL(t+".js",n).href,i[t]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=t,e.onload=i,document.head.appendChild(e)}else e=t,importScripts(t),i()}).then(()=>{let e=i[t];if(!e)throw new Error(`Module ${t} didnt register its module`);return e}));self.define=(n,r)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let o={};const c=e=>t(e,s),d={module:{uri:s},exports:o,require:c};i[s]=Promise.all(n.map(e=>d[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-5ffe50d4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"12a559b4be7cb9d5a4aa8caa0d4b73fa"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
if(!self.define){let e,i={};const t=(t,n)=>(t=new URL(t+".js",n).href,i[t]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=t,e.onload=i,document.head.appendChild(e)}else e=t,importScripts(t),i()}).then(()=>{let e=i[t];if(!e)throw new Error(`Module ${t} didnt register its module`);return e}));self.define=(n,r)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let o={};const c=e=>t(e,s),d={module:{uri:s},exports:o,require:c};i[s]=Promise.all(n.map(e=>d[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-5ffe50d4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"2002c5d0700ecb765d710eec149cf502"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});

View File

@@ -16,7 +16,7 @@
</style>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body class="bg-white text-gray-900 selection:bg-black/10" hx-on="click: if(!document.getElementById('threadPopover').contains(event.target)&&!event.target.closest('[data-thread-menu]')) hideThreadPopover(); if(!document.getElementById('sunePopover').contains(event.target)&&!event.target.closest('[data-sune-menu]')) hideSunePopover(); if(!document.getElementById('userMenu').contains(event.target)&&!document.getElementById('userMenuBtn').contains(event.target)) document.getElementById('userMenu').classList.add('hidden')">
<body class="bg-white text-gray-900 selection:bg-black/10" hx-on="click: if(event.target.closest('button')) haptic(); if(!document.getElementById('threadPopover').contains(event.target)&&!event.target.closest('[data-thread-menu]')) hideThreadPopover(); if(!document.getElementById('sunePopover').contains(event.target)&&!event.target.closest('[data-sune-menu]')) hideSunePopover(); if(!document.getElementById('userMenu').contains(event.target)&&!document.getElementById('userMenuBtn').contains(event.target)) document.getElementById('userMenu').classList.add('hidden')">
<div class="flex flex-col h-dvh max-h-dvh">
<header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
@@ -124,7 +124,7 @@
<div id="accountPanelGeneral" class="p-4 space-y-4">
<div><label class="block text-gray-700 font-medium mb-1">Provider</label><select id="set_provider" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="openrouter">OpenRouter</option><option value="openai">OpenAI</option></select><p class="mt-1 text-xs text-gray-500">Or you can just prefix names with or: or oai: to override.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" 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><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it" value="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
</div>
<div id="accountPanelAPI" class="p-4 space-y-4 hidden">
<div><label class="block text-gray-700 font-medium mb-1">OpenRouter API Key</label><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-or-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
@@ -148,6 +148,7 @@
const DEFAULT_MODEL='openai/gpt-5',DEFAULT_API_KEY=''
const el=Object.fromEntries(['topbar','chat','messages','composer','input','sendBtn','suneBtnTop','suneModal','suneURL','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','sidebarLeft','sidebarOverlayLeft','sidebarBtnLeft','suneList','newSuneBtn','userMenuBtn','userMenu','accountSettingsOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','sidebarBtnRight','sidebarRight','sidebarOverlayRight','threadList','closeThreads','threadPopover','sunePopover','footer','attachBtn','attachBadge','fileInput','htmlEditor','extensionHtmlEditor','htmlTab_index','htmlTab_extension','suneHtml','accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','set_master_prompt','set_provider','set_api_key_or','set_api_key_oai','set_title_model','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','accountTabGeneral','accountTabAPI','accountPanelGeneral','accountPanelAPI','set_gh_token','importAccountSettings','exportAccountSettings','importAccountSettingsInput'].map(id=>[id,document.getElementById(id)]))
const icons=()=>window.lucide&&lucide.createIcons()
const haptic=()=>/android/i.test(navigator.userAgent)&&navigator.vibrate?.(4)
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 sid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6)
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]}