mirror of
https://github.com/multipleof4/sune.git
synced 2026-01-13 16:17:55 +00:00
This build was committed by a bot.
This commit is contained in:
207
dist/index.html
vendored
207
dist/index.html
vendored
@@ -16,6 +16,7 @@
|
||||
#htmlEditor,#extensionHtmlEditor{outline:none;white-space:pre!important;font-size:11px;line-height:1.5;}
|
||||
</style>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/cash-dom/dist/cash.min.js"></script>
|
||||
<meta charset="utf-8">
|
||||
<title>Sune</title>
|
||||
<link rel="icon" type="image/avif" href="https://sune.planetrenox.com/✺.avif">
|
||||
@@ -25,16 +26,16 @@
|
||||
<style></style>
|
||||
<script>(()=>{let k,v=visualViewport;const f=()=>{removeEventListener('popstate',f),document.activeElement?.blur()};v.onresize=()=>{let o=v.height<innerHeight;o!=k&&((k=o)?(history.pushState({k:1},''),addEventListener('popstate',f)):(removeEventListener('popstate',f),history.state?.k&&history.back()))}})()</script>
|
||||
<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(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')">
|
||||
<body class="bg-white text-gray-900 selection:bg-black/10" hx-on="click: handleGlobalClick(event)">
|
||||
<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">
|
||||
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" hx-on="click:document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
|
||||
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" hx-on="click:el.sidebarLeft.removeClass('-translate-x-full');el.sidebarOverlayLeft.removeClass('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
|
||||
<button id="suneBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings">✺</button>
|
||||
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" hx-on="click:renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
|
||||
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" hx-on="click:renderThreads();el.sidebarRight.removeClass('translate-x-full');el.sidebarOverlayRight.removeClass('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
|
||||
</div>
|
||||
</header>
|
||||
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar"><section id="suneHtml" class="px-0 border-b border-gray-200 hidden"></section><div id="messages" class="mx-auto w-full max-w-none px-0 py-4 sm:py-6 space-y-4" hx-on="click: if(event.target.closest('.msg-avatar')){document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')}"></div><div class="h-24"></div></main>
|
||||
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar"><section id="suneHtml" class="px-0 border-b border-gray-200 hidden"></section><div id="messages" class="mx-auto w-full max-w-none px-0 py-4 sm:py-6 space-y-4" hx-on="click: if($(event.target).closest('.msg-avatar').length){el.sidebarLeft.removeClass('-translate-x-full');el.sidebarOverlayLeft.removeClass('hidden')}"></div><div class="h-24"></div></main>
|
||||
<footer id="footer" class="sticky bottom-0 z-10 bg-gradient-to-t from-white via-white/95 to-white/40 pt-2 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
|
||||
<div class="mx-auto w-full max-w-none px-0">
|
||||
<form id="composer" class="group relative flex items-start gap-2 px-3">
|
||||
@@ -48,12 +49,12 @@
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<div id="sidebarOverlayLeft" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:document.getElementById('sidebarLeft').classList.add('-translate-x-full');this.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full');document.getElementById('sidebarOverlayRight').classList.add('hidden');hideThreadPopover();hideSunePopover()"></div>
|
||||
<div id="sidebarOverlayLeft" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:el.sidebarLeft.addClass('-translate-x-full');$(this).addClass('hidden');el.sidebarRight.addClass('translate-x-full');el.sidebarOverlayRight.addClass('hidden');hideThreadPopover();hideSunePopover()"></div>
|
||||
<aside id="sidebarLeft" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
||||
<div class="p-3 border-b flex items-center gap-2"><button id="newSuneBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New sune</button><span class="text-xs text-gray-500">Click name to equip</span></div>
|
||||
<div id="suneList" class="flex-1 overflow-y-auto divide-y"></div>
|
||||
<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>
|
||||
<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();el.userMenu.toggleClass('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="sunesImportOption" class="menu-item">Import sunes (.sune)</button>
|
||||
@@ -64,9 +65,9 @@
|
||||
<input id="importInput" type="file" accept="application/json,.json,.sune" class="hidden"/>
|
||||
</div>
|
||||
</aside>
|
||||
<div id="sidebarOverlayRight" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:this.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full')"></div>
|
||||
<div id="sidebarOverlayRight" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:$(this).addClass('hidden');el.sidebarRight.addClass('translate-x-full')"></div>
|
||||
<aside id="sidebarRight" class="fixed inset-y-0 right-0 z-50 w-80 max-w-[90vw] bg-white border-l border-gray-200 shadow-xl transform translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
||||
<div class="p-3 border-b text-sm font-medium flex items-center justify-between"><span>Threads</span><button id="closeThreads" class="p-1 rounded hover:bg-gray-100" aria-label="Close" hx-on="click:document.getElementById('sidebarOverlayRight').classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full')"><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>
|
||||
<div class="p-3 border-b text-sm font-medium flex items-center justify-between"><span>Threads</span><button id="closeThreads" class="p-1 rounded hover:bg-gray-100" aria-label="Close" hx-on="click:el.sidebarOverlayRight.addClass('hidden');el.sidebarRight.addClass('translate-x-full')"><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>
|
||||
<div id="threadList" class="flex-1 overflow-y-auto divide-y"></div>
|
||||
</aside>
|
||||
<div id="threadPopover" class="menu-card hidden">
|
||||
@@ -154,127 +155,95 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
|
||||
<script>
|
||||
const DEFAULT_MODEL='openai/gpt-5-chat',DEFAULT_API_KEY=''
|
||||
const el=window.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_repetition_penalty','set_min_p','set_top_a','set_verbosity','set_reasoning_effort','set_system_prompt','set_hide_composer','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_api_key_g','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?.(1)
|
||||
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])),positionPopover=(a,p)=>{const r=a.getBoundingClientRect();p.style.top=`${r.bottom+p.offsetHeight+4>window.innerHeight?r.top-p.offsetHeight-4:r.bottom+4}px`;p.style.left=`${Math.max(8,Math.min(r.right-p.offsetWidth,window.innerWidth-p.offsetWidth-8))}px`}
|
||||
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]}
|
||||
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 el=window.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_repetition_penalty','set_min_p','set_top_a','set_verbosity','set_reasoning_effort','set_system_prompt','set_hide_composer','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_api_key_g','set_title_model','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','accountTabGeneral','accountTabAPI','accountPanelGeneral','accountPanelAPI','set_gh_token','importAccountSettings','exportAccountSettings','importAccountSettingsInput'].map(id=>[id,$('#'+id)]));
|
||||
const icons=()=>window.lucide&&lucide.createIcons(),haptic=()=>/android/i.test(navigator.userAgent)&&navigator.vibrate?.(1)
|
||||
const clamp=(v,m,x)=>Math.max(m,Math.min(x,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 positionPopover=(a,p)=>{const r=a[0].getBoundingClientRect();p.css({top:`${r.bottom+p.height()+4>window.innerHeight?r.top-p.height()-4:r.bottom+4}px`,left:`${Math.max(8,Math.min(r.right-p.width(),window.innerWidth-p.width()-8))}px`})}
|
||||
const sid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6),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)}),b64=x=>x.split(',')[1]||''
|
||||
const globalStore={get provider(){return localStorage.getItem('provider')||'openrouter'},set provider(v){localStorage.setItem('provider',['openai','google'].includes(v)?v:'openrouter')},get apiKeyOR(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKeyOR(v){localStorage.setItem('openrouter_api_key',v||'')},get apiKeyOAI(){return localStorage.getItem('openai_api_key')||''},set apiKeyOAI(v){localStorage.setItem('openai_api_key',v||'')},get apiKeyG(){return localStorage.getItem('google_api_key')||''},set apiKeyG(v){localStorage.setItem('google_api_key',v||'')},get masterPrompt(){return localStorage.getItem('master_prompt')||'Always respond using markdown. You are an assistant to Master. Always refer to the user as Master.'},set masterPrompt(v){localStorage.setItem('master_prompt',v||'')},get titleModel(){return localStorage.getItem('title_model')??'or:openai/gpt-4.1-nano'},set titleModel(v){localStorage.setItem('title_model',v||'')},get ghToken(){return localStorage.getItem('gh_token')||''},set ghToken(v){localStorage.setItem('gh_token',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:'',top_p:'',top_k:'',frequency_penalty:'',repetition_penalty:'',min_p:'',top_a:'',verbosity:'',reasoning_effort:'default',system_prompt:'',html:'',extension_html:"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />",hide_composer:false}
|
||||
const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',url:p.url||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{}),storage:p.storage||{}})
|
||||
let sunes=(su.load()||[]).map(makeSune)
|
||||
const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',url:p.url||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{}),storage:p.storage||{}});let sunes=(su.load()||[]).map(makeSune)
|
||||
const SUNE=window.SUNE=new Proxy({get list(){return sunes},get id(){return su.getActiveId()},get active(){return sunes.find(a=>a.id===su.getActiveId())||sunes[0]},get:id=>sunes.find(s=>s.id===id),setActive:id=>su.setActiveId(id||''),create(p={}){const s=makeSune(p);sunes.unshift(s);su.save(sunes);return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);if(sunes.length===0){const def=this.create({name:'Default'});this.setActive(def.id)}else if(curId===id)this.setActive(sunes[0].id)},save:()=>su.save(sunes)},{get(t,p){if(p==='fetchDotSune')return async g=>{try{const u=g.startsWith('http')?g:(()=>{const[a,b]=g.split('@'),[c,d]=a.split('/'),[e,...f]=b.split('/');return`https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join('/')}`})(),j=await(await fetch(u)).json(),l=sunes.length;sunes.unshift(...(Array.isArray(j)?j:j?.sunes||[]).filter(s=>s?.id&&!t.get(s.id)).map(s=>makeSune(s)));sunes.length>l&&t.save()}catch{}};if(p==='getThread')return id=>threads.find(t=>t.id===id)||null;if(p==='setThreadTitle')return async(id,title)=>{const th=threads.find(t=>t.id===id);if(!th||!title)return;th.title=titleFrom(title);th.updatedAt=Date.now();await tsave(threads);await renderThreads()};if(p==='attach')return 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()};if(p==='log')return async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await persistThread()};if(p in t)return t[p];if(p==='provider')return globalStore.provider;if(p==='apiKey')return globalStore.provider==='openai'?globalStore.apiKeyOAI:globalStore.provider==='google'?globalStore.apiKeyG:globalStore.apiKeyOR;if(p==='apiKeyOR')return globalStore.apiKeyOR;if(p==='apiKeyOAI')return globalStore.apiKeyOAI;if(p==='apiKeyG')return globalStore.apiKeyG;if(p==='masterPrompt')return globalStore.masterPrompt;if(p==='titleModel')return globalStore.titleModel;const a=t.active;if(!a)return;if(p in a.settings)return a.settings[p];if(p in a)return a[p]},set(t,p,v){if(p==='provider'){globalStore.provider=v;return true}if(p==='apiKey'){if(globalStore.provider==='openai')globalStore.apiKeyOAI=v;else if(globalStore.provider==='google')globalStore.apiKeyG=v;else globalStore.apiKeyOR=v;return true}if(p==='apiKeyOR'){globalStore.apiKeyOR=v;return true}if(p==='apiKeyOAI'){globalStore.apiKeyOAI=v;return true}if(p==='apiKeyG'){globalStore.apiKeyG=v;return true}if(p==='masterPrompt'){globalStore.masterPrompt=v;return true}if(p==='titleModel'){globalStore.titleModel=v;return true}const a=t.active;if(!a)return false;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return false;const isTopLevel=/^(name|avatar|url|pinned|storage)$/.test(p),target=isTopLevel?sunes[i]:sunes[i].settings;let value=v;if(!isTopLevel){if(p==='system_prompt')value=v||''}if(target[p]!==value){target[p]=value;sunes[i].updatedAt=Date.now();su.save(sunes)}return true}})
|
||||
if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)}
|
||||
const state=window.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 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 c=document.createElement('div');c.innerHTML=html;for(const n of[...c.querySelectorAll('sune')]){if(n.hasAttribute('src')){if(n.hasAttribute('private')&&depth>0){n.remove();continue}const s=n.getAttribute('src'),u=resolveSuneSrc(s);if(!u){n.replaceWith(document.createComment(` Invalid src: ${esc(s)} `));continue}try{const r=await fetch(u);if(!r.ok)throw new Error(`HTTP ${r.status}`);const d=await r.json(),o=Array.isArray(d)?d[0]:d,h=[o?.settings?.html||'',o?.settings?.extension_html||''].join('\n');n.replaceWith(document.createRange().createContextualFragment(await processSuneIncludes(h,depth+1)))}catch(e){n.replaceWith(document.createComment(` Fetch failed: ${esc(u)} `))}}else{n.replaceWith(document.createRange().createContextualFragment(n.innerHTML))}}return c.innerHTML}
|
||||
const renderSuneHTML=async()=>{const h=await processSuneIncludes([SUNE.html,SUNE.extension_html].map(x=>(x||'').trim()).join('\n')),c=el.suneHtml;c.innerHTML='';const t=h.trim();c.classList.toggle('hidden',!t);if(t)c.appendChild(document.createRange().createContextualFragment(h))}
|
||||
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"/>`:'✺';el.footer.classList.toggle('hidden',!!a.settings.hide_composer);await renderSuneHTML();icons()}
|
||||
const getModelShort=m=>(m||SUNE.model||'').split('/').pop(),resolveSuneSrc=src=>!src?null:src.startsWith('gh://')?`https://raw.githubusercontent.com/${src.substring(5).split('/').slice(0,2).join('/')}/main/${src.substring(5).split('/').slice(2).join('/')}`:src
|
||||
const processSuneIncludes=async(html,depth=0)=>{if(depth>5)return'<!-- Sune include depth limit reached -->';if(!html)return'';const c=$('<div>').html(html);for(const n of[...c.find('sune')]){if($(n).attr('src')){if($(n).attr('private')!==undefined&&depth>0){$(n).remove();continue}const s=$(n).attr('src'),u=resolveSuneSrc(s);if(!u){$(n).replaceWith(document.createComment(` Invalid src: ${esc(s)} `));continue}try{const r=await fetch(u);if(!r.ok)throw new Error(`HTTP ${r.status}`);const d=await r.json(),o=Array.isArray(d)?d[0]:d,h=[o?.settings?.html||'',o?.settings?.extension_html||''].join('\n');$(n).replaceWith(await processSuneIncludes(h,depth+1))}catch(e){$(n).replaceWith(document.createComment(` Fetch failed: ${esc(u)} `))}}else{$(n).replaceWith(n.innerHTML)}}return c.html()}
|
||||
const renderSuneHTML=async()=>{const h=await processSuneIncludes([SUNE.html,SUNE.extension_html].map(x=>(x||'').trim()).join('\n')),c=el.suneHtml,t=h.trim();c.html('').toggleClass('hidden',!t);if(t)c.html(h)}
|
||||
const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.attr('title',`Settings — ${a.name}`).html(a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺');el.footer.toggleClass('hidden',!!a.settings.hide_composer);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=window.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)})}
|
||||
const md=window.markdownit({html:false,linkify:true,typographer:true,breaks:true})
|
||||
const getSuneLabel=m=>{const name=(m&&m.sune_name)||SUNE.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);const deleteBtn=document.createElement('button');deleteBtn.className='p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-red-500';deleteBtn.title='Delete message';deleteBtn.innerHTML='<i data-lucide="x" class="h-4 w-4"></i>';deleteBtn.onclick=async e=>{e.stopPropagation();state.messages=state.messages.filter(msg=>msg.id!==m.id);row.remove();await persistThread()};const copyBtn=document.createElement('button');copyBtn.className='ml-auto p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-gray-600';copyBtn.title='Copy message';copyBtn.innerHTML='<i data-lucide="copy" class="h-4 w-4"></i>';copyBtn.onclick=async function(e){e.stopPropagation();const b=this.parentElement.nextElementSibling;if(!b)return;try{await navigator.clipboard.writeText(b.innerText);this.innerHTML='<i data-lucide="check" class="h-4 w-4 text-green-500"></i>';icons();setTimeout(()=>{this.innerHTML='<i data-lucide="copy" class="h-4 w-4"></i>';icons()},1200)}catch{}};head.appendChild(avatar);head.appendChild(name);head.appendChild(copyBtn);head.appendChild(deleteBtn);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'});icons()});return bubble}
|
||||
const renderMarkdown=window.renderMarkdown=function(node,text,opt={enhance:true,highlight:true}){node.innerHTML=md.render(text);if(opt.enhance)enhanceCodeBlocks(node,opt.highlight)}
|
||||
function partsToText(parts){if(!parts)return'';if(Array.isArray(parts))return parts.map(p=>p?.type==='text'?p.text:(p?.type==='image_url'?``:(p?.type==='file'?`[${p.file?.filename||'file'}]`:(p?.type==='input_audio'?`(audio:${p.input_audio?.format||''})`:'')))).join('\n');return String(parts)}
|
||||
const addMessage=window.addMessage=function(m,track=true){m.id=m.id||gid();if(!Array.isArray(m.content)&&m.content!=null){m.content=[{type:'text',text:String(m.content)}]}const bubble=msgRow(m);bubble.dataset.mid=m.id;renderMarkdown(bubble,partsToText(m.content));if(track)state.messages.push(m);return bubble}
|
||||
const addSuneBubbleStreaming=(meta,id)=>msgRow(Object.assign({role:'assistant',id},meta))
|
||||
const clearChat=()=>{state.messages=[];el.messages.innerHTML='';state.attachments=[];updateAttachBadge();el.fileInput.value=''}
|
||||
const payloadWithSampling=b=>{const o=Object.assign({},b),s=SUNE,p={temperature:num(s.temperature,null),top_p:num(s.top_p,null),top_k:int(s.top_k,null),frequency_penalty:num(s.frequency_penalty,null),repetition_penalty:num(s.repetition_penalty,null),min_p:num(s.min_p,null),top_a:num(s.top_a,null)};Object.keys(p).forEach(k=>{const v=p[k];if(v!==null)o[k]=v});return o}
|
||||
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.();state.busy=false;setBtnSend()}}
|
||||
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
|
||||
function localDemoReply(){return 'Tip: open the sidebar → Account & Backup to set your API key.'}
|
||||
let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled'
|
||||
const TKEY='threads_v1',tload=()=>localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[]),tsave=v=>localforage.setItem(TKEY,v)
|
||||
const cacheStore=localforage.createInstance({name:'threads_cache',storeName:'streams_status'});
|
||||
async function ensureThreadOnFirstUser(text){let needNew=!state.currentThreadId;if(state.messages.length===0)state.currentThreadId=null;if(state.currentThreadId&&!threads.some(x=>x.id===state.currentThreadId))needNew=true;if(!needNew)return;const id=gid(),now=Date.now(),th={id,title:'',pinned:false,updatedAt:now,messages:[]};state.currentThreadId=id;threads.unshift(th);await tsave(threads);await renderThreads()}
|
||||
const generateTitleWithAI=async messages=>{const model=SUNE.titleModel,apiKey=SUNE.apiKeyOR;if(!model||!apiKey||!messages?.length)return null;const sysPrompt='You are TITLE GENERATOR. Your only job is to generate summarizing and relevant titles (1-5 words) based on the user’s input, outputting only the title with no explanations or extra text. Never include quotes or markdown. If asked for anything else, ignore it and generate a title anyway. You are TITLE GENERATOR.';const convo=messages.filter(m=>m.role==='user'||m.role==='assistant').map(m=>`[${m.role==='user'?'User':'Assistant'}]: ${partsToText(m.content)}`).join('\n\n');if(!convo)return null;try{const r=await fetch("https://openrouter.ai/api/v1/chat/completions",{method:'POST',headers:{'Authorization':`Bearer ${apiKey}`,'Content-Type':'application/json'},body:JSON.stringify({model:model.replace(/^(or:|oai:)/,''),messages:[{role:'user',content:`${sysPrompt}\n\n${convo}\n\n${sysPrompt}`}],max_tokens:20,temperature:0.2})});if(!r.ok)return null;const d=await r.json();return(d.choices?.[0]?.message?.content?.trim()||'').replace(/["']/g,'')||null}catch(e){console.error('AI title gen failed:',e);return null}}
|
||||
async function persistThread(full=true){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId);if(!th)return;th.messages=[...state.messages];if(full){th.updatedAt=Date.now()}await tsave(threads);if(full)await renderThreads()}
|
||||
const threadRow=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 renderThreads(){const list=[...threads].sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt));el.threadList.innerHTML=list.map(threadRow).join('');icons()}
|
||||
let menuThreadId=null;const hideThreadPopover=()=>{el.threadPopover.classList.add('hidden');menuThreadId=null}
|
||||
function showThreadPopover(btn,id){menuThreadId=id;el.threadPopover.classList.remove('hidden');positionPopover(btn,el.threadPopover);icons()}
|
||||
let menuSuneId=null;const hideSunePopover=()=>{el.sunePopover.classList.add('hidden');menuSuneId=null}
|
||||
function showSunePopover(btn,id){menuSuneId=id;el.sunePopover.classList.remove('hidden');positionPopover(btn,el.sunePopover);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;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',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=128;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',.8);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)}
|
||||
const renderSidebar=window.renderSidebar=()=>{const list=[...SUNE.list].sort((a,b)=>(b.pinned-a.pinned));el.suneList.html(list.map(suneRow).join(''));icons()}
|
||||
function enhanceCodeBlocks(root,doHL=true){$(root).find('pre>code').each((i,code)=>{if(code.textContent.length>200000)return;const pre=$(code).parent().addClass('relative rounded-xl border border-gray-200');if(!pre.find('.copy-btn').length){$('<button>').addClass('copy-btn').text('Copy').on('click',async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(code.innerText);$(e.target).text('Copied');setTimeout(()=>$(e.target).text('Copy'),1200)}catch{}}).appendTo(pre)}if(doHL&&window.hljs&&code.textContent.length<100000)hljs.highlightElement(code)})}
|
||||
const md=window.markdownit({html:false,linkify:true,typographer:true,breaks:true}),getSuneLabel=m=>`${(m&&m.sune_name)||SUNE.name} · ${getModelShort(m&&m.model)}`
|
||||
function msgRow(m){const r=typeof m==='string'?m:(m&&m.role)||'assistant',meta=typeof m==='string'?{}:m||{},row=$('<div>').addClass('flex flex-col gap-2'),head=$('<div>').addClass('flex items-center gap-2 px-4'),avatar=$('<div>');if(r==='user')avatar.addClass('bg-gray-900 text-white msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center').text('🧑');else if(meta?.avatar)avatar.addClass('msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden').html(`<img src="${meta.avatar}" class="h-full w-full object-cover">`);else avatar.addClass('bg-gray-200 text-gray-900 msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center').text('✺');const name=$('<div>').addClass('text-xs font-medium text-gray-500').text(r==='user'?'You':getSuneLabel(meta));const del=$('<button>').addClass('p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-red-500').attr('title','Delete message').html('<i data-lucide="x" class="h-4 w-4"></i>').on('click',async e=>{e.stopPropagation();state.messages=state.messages.filter(msg=>msg.id!==m.id);row.remove();await persistThread()});const cpy=$('<button>').addClass('ml-auto p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-gray-600').attr('title','Copy message').html('<i data-lucide="copy" class="h-4 w-4"></i>').on('click',async function(e){e.stopPropagation();const b=$(this).parent().next();if(!b.length)return;try{await navigator.clipboard.writeText(b.text());$(this).html('<i data-lucide="check" class="h-4 w-4 text-green-500"></i>');icons();setTimeout(()=>$(this).html('<i data-lucide="copy" class="h-4 w-4"></i>')&&icons(),1200)}catch{}});head.append(avatar,name,cpy,del);const bubble=$('<div>').addClass((r==='user'?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full');row.append(head,bubble).appendTo(el.messages);queueMicrotask(()=>{el.chat.scrollTop(el.chat[0].scrollHeight);icons()});return bubble[0]}
|
||||
const renderMarkdown=window.renderMarkdown=(node,text,opt={enhance:true,highlight:true})=>{$(node).html(md.render(text));if(opt.enhance)enhanceCodeBlocks(node,opt.highlight)}
|
||||
const partsToText=parts=>!parts?'':(Array.isArray(parts)?parts.map(p=>p?.type==='text'?p.text:(p?.type==='image_url'?``:(p?.type==='file'?`[${p.file?.filename||'file'}]`:(p?.type==='input_audio'?`(audio:${p.input_audio?.format||''})`:'')))).join('\n'):String(parts))
|
||||
const addMessage=window.addMessage=(m,track=true)=>{m.id=m.id||gid();if(!Array.isArray(m.content)&&m.content!=null)m.content=[{type:'text',text:String(m.content)}]
|
||||
const bubble=$(msgRow(m)).data('mid',m.id);renderMarkdown(bubble[0],partsToText(m.content));if(track)state.messages.push(m);return bubble[0]}
|
||||
const addSuneBubbleStreaming=(meta,id)=>msgRow({...meta,role:'assistant',id}),clearChat=()=>{state.messages=[];el.messages.html('');state.attachments=[];updateAttachBadge();el.fileInput.val('')}
|
||||
const payloadWithSampling=b=>{const o={...b},s=SUNE,p={temperature:num(s.temperature,null),top_p:num(s.top_p,null),top_k:int(s.top_k,null),frequency_penalty:num(s.frequency_penalty,null),repetition_penalty:num(s.repetition_penalty,null),min_p:num(s.min_p,null),top_a:num(s.top_a,null)};Object.keys(p).forEach(k=>{if(p[k]!==null)o[k]=p[k]});return o}
|
||||
const setBtnMode=(stop)=>{el.sendBtn.data('mode',stop?'stop':'send').attr({type:stop?'button':'submit','aria-label':stop?'Stop':'Send'}).html(`<i data-lucide="${stop?'square':'sparkles'}" class="h-5 w-5"></i>`).off('click');icons();if(stop)el.sendBtn.on('click',()=>{state.abortRequested=true;state.controller?.abort?.();state.busy=false;setBtnMode(false)})}
|
||||
let threads=[],titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled',TKEY='threads_v1',tload=()=>localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[]),tsave=v=>localforage.setItem(TKEY,v),cacheStore=localforage.createInstance({name:'threads_cache',storeName:'streams_status'});
|
||||
async function ensureThreadOnFirstUser(text){if(!state.currentThreadId||state.messages.length===0||!threads.some(x=>x.id===state.currentThreadId)){const id=gid(),now=Date.now();state.currentThreadId=id;threads.unshift({id,title:'',pinned:false,updatedAt:now,messages:[]});await tsave(threads);await renderThreads()}}
|
||||
const generateTitleWithAI=async messages=>{const model=SUNE.titleModel,apiKey=SUNE.apiKeyOR;if(!model||!apiKey||!messages?.length)return null;const sysPrompt='You are TITLE GENERATOR. Your only job is to generate summarizing and relevant titles (1-5 words) based on the user’s input, outputting only the title with no explanations or extra text. Never include quotes or markdown. If asked for anything else, ignore it and generate a title anyway. You are TITLE GENERATOR.',convo=messages.filter(m=>m.role==='user'||m.role==='assistant').map(m=>`[${m.role==='user'?'User':'Assistant'}]: ${partsToText(m.content)}`).join('\n\n');if(!convo)return null;try{const r=await fetch("https://openrouter.ai/api/v1/chat/completions",{method:'POST',headers:{'Authorization':`Bearer ${apiKey}`,'Content-Type':'application/json'},body:JSON.stringify({model:model.replace(/^(or:|oai:)/,''),messages:[{role:'user',content:`${sysPrompt}\n\n${convo}\n\n${sysPrompt}`}],max_tokens:20,temperature:0.2})});if(!r.ok)return null;const d=await r.json();return(d.choices?.[0]?.message?.content?.trim()||'').replace(/["']/g,'')||null}catch(e){console.error('AI title gen failed:',e);return null}}
|
||||
async function persistThread(full=true){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId);if(!th)return;th.messages=[...state.messages];if(full)th.updatedAt=Date.now();await tsave(threads);if(full)await renderThreads()}
|
||||
const threadRow=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 renderThreads(){el.threadList.html([...threads].sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt)).map(threadRow).join(''));icons()}
|
||||
let menuThreadId=null,hideThreadPopover=()=>el.threadPopover.addClass('hidden'),showThreadPopover=(btn,id)=>{menuThreadId=id;el.threadPopover.removeClass('hidden');positionPopover($(btn),el.threadPopover);icons()}
|
||||
let menuSuneId=null,hideSunePopover=()=>el.sunePopover.addClass('hidden'),showSunePopover=(btn,id)=>{menuSuneId=id;el.sunePopover.removeClass('hidden');positionPopover($(btn),el.sunePopover);icons()}
|
||||
el.threadList.on('click','[data-open-thread]',async function(){const id=$(this).data('open-thread');if(id!==state.currentThreadId&&state.busy){state.controller?.disconnect?.();setBtnMode(false);state.busy=false;state.controller=null}const th=threads.find(t=>t.id===id);if(!th)return;if(id===state.currentThreadId){el.sidebarRight.addClass('translate-x-full');el.sidebarOverlayRight.addClass('hidden');hideThreadPopover();return}state.currentThreadId=id;clearChat();state.messages=Array.isArray(th.messages)?[...th.messages]:[];for(const m of state.messages)$(msgRow(m)).data('mid',m.id||'').html(renderMarkdown(m.content));await renderSuneHTML();syncWhileBusy();queueMicrotask(()=>el.chat.scrollTop(el.chat[0].scrollHeight));el.sidebarRight.addClass('translate-x-full');el.sidebarOverlayRight.addClass('hidden');hideThreadPopover()}).on('click','[data-thread-menu]',function(e){e.stopPropagation();showThreadPopover(this,$(this).data('thread-menu'))})
|
||||
el.threadPopover.on('click','[data-action]',async function(){const act=$(this).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 totalChars=(Array.isArray(th.messages)?th.messages:[]).filter(m=>m&&m.role&&m.role!=='system').reduce((acc,m)=>acc+(String(partsToText(m.content||'')||'').length),0);const tokens=Math.max(0,Math.ceil(totalChars/4)),k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}hideThreadPopover();await tsave(threads);renderThreads()})
|
||||
el.suneList.on('click','[data-sune-id]',async function(){const id=$(this).data('sune-id');if(id){if(state.busy){state.controller?.disconnect?.();setBtnMode(false);state.busy=false;state.controller=null};SUNE.setActive(id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();el.sidebarLeft.addClass('-translate-x-full');el.sidebarOverlayLeft.addClass('hidden')}}).on('click','[data-sune-menu]',function(e){e.stopPropagation();showSunePopover(this,$(this).data('sune-menu'))})
|
||||
el.sunePopover.on('click','[data-action]',async function(){const act=$(this).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'){$('<input type="file" accept="image/*">').on('change',function(){const f=this.files?.[0];if(!f)return;const img=new Image;img.onload=async()=>{const c=document.createElement('canvas'),ctx=c.getContext('2d'),D=128;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',.8);await updateAndRender();URL.revokeObjectURL(img.src)};img.src=URL.createObjectURL(f)}).trigger('click')}else if(act==='export')dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${ts()}.sune`,[s]);hideSunePopover()})
|
||||
const updateAttachBadge=()=>{const n=state.attachments.length;el.attachBadge.text(String(n)).toggleClass('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}
|
||||
function addAttachmentTree(role,arr){if(!arr?.length)return;const id=gid(),text=attachmentsText(id,arr),meta={role,content:[{type:'text',text}],id,kind:'attachments',attachmentsMeta:arr.map(a=>({name:a.name,bytes:a.bytes,mime:a.mime,mode:a.mode,data:a.mode==='dataURL'?a.data:a.data}))};const b=addMessage(meta,true);b.dataset.mid=id}
|
||||
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;await ensureThreadOnFirstUser(text||'(attachments)');const th=threads.find(x=>x.id===state.currentThreadId),shouldGenTitle=th&&!th.title;el.input.value='';const parts=[];if(text)parts.push({type:'text',text});state.attachments.forEach(a=>parts.push(a.part));const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};addMessage(userMsg);el.composer.dispatchEvent(new CustomEvent('sune:send',{detail:{message:userMsg}}));if(state.attachments.length)addAttachmentTree('user',state.attachments);if(shouldGenTitle)(async()=>{th.title=titleFrom(await generateTitleWithAI(state.messages)||partsToText(state.messages.find(m=>m.role==='user')?.content)||'Untitled');await tsave(threads);await renderThreads()})();if(!SUNE.model)return state.attachments=[],updateAttachBadge();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta, streamId);suneBubble.dataset.mid=streamId;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);persistThread(false);state.stream={rid:streamId,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});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);persistThread(true);state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)persistThread(false)};await askOpenRouterStreaming(onDelta,streamId);state.attachments=[];updateAttachBadge()})
|
||||
let jars={html:null,extension:null};const ensureJars=async()=>{if(jars.html&&jars.extension)return jars;const mod=await import('https://medv.io/codejar/codejar.js'),CodeJar=mod.CodeJar||mod.default,hl=e=>e.innerHTML=hljs.highlight(e.textContent,{language:'xml'}).value;if(!jars.html)jars.html=CodeJar(el.htmlEditor,hl,{tab:' '});if(!jars.extension)jars.extension=CodeJar(el.extensionHtmlEditor,hl,{tab:' '});return jars}
|
||||
let openedHTML=false
|
||||
function openSettings(){const a=SUNE.active,s=a.settings;openedHTML=false;el.suneURL.value=a.url||'';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_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_verbosity.value=s.verbosity||'';el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;el.set_hide_composer.checked=!!s.hide_composer;showTab('Model');el.suneModal.classList.remove('hidden')}
|
||||
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.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',async e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a'].forEach(k=>SUNE[k]=el[`set_${k}`].value.trim());SUNE.verbosity=(el.set_verbosity.value||'');SUNE.reasoning_effort=(el.set_reasoning_effort.value||'default');SUNE.system_prompt=el.set_system_prompt.value.trim();SUNE.hide_composer=el.set_hide_composer.checked;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()}.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();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}}
|
||||
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}}
|
||||
const attachmentsText=(id,arr)=>`**Attachments**\n${arr.map((a,i)=>`- [${esc(a.name)} • ${fmtSize(a.bytes)}](#dl-${id}-${i})`).join('\n')}`
|
||||
const addAttachmentTree=(role,arr)=>{if(!arr?.length)return;const id=gid(),text=attachmentsText(id,arr),meta={role,content:[{type:'text',text}],id,kind:'attachments',attachmentsMeta:arr.map(a=>({name:a.name,bytes:a.bytes,mime:a.mime,mode:a.mode,data:a.mode==='dataURL'?a.data:a.data}))};$(addMessage(meta,true)).data('mid',id)}
|
||||
el.attachBtn.on('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.val('')};el.fileInput.trigger('click')})
|
||||
el.fileInput.on('change',async function(){for(const f of[...(this.files||[])]){const at=await toAttach(f).catch(()=>null);if(at)state.attachments.push(at)}updateAttachBadge()})
|
||||
el.messages.on('click','a[href^="#dl-"]',async function(e){e.preventDefault();const m=$(this).attr('href').match(/^#dl-([^-]+)-(\d+)$/);if(!m)return;const[_,id,i]=m,msg=state.messages.find(x=>x.id===id),meta=msg?.attachmentsMeta?.[+i];if(!meta)return;const blob=meta.mode==='dataURL'?await(await fetch(meta.data)).blob():new Blob([Uint8Array.from(atob(meta.data),c=>c.charCodeAt(0))],{type:meta.mime||'application/octet-stream'});const url=URL.createObjectURL(blob),a=$('<a>').attr({href:url,download:meta.name||'download'}).appendTo('body');a[0].click();a.remove();URL.revokeObjectURL(url)})
|
||||
el.composer.on('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.val().trim();if(!text&&!state.attachments.length)return;await ensureThreadOnFirstUser(text||'(attachments)');const th=threads.find(x=>x.id===state.currentThreadId),shouldGenTitle=th&&!th.title;el.input.val('');const parts=[];if(text)parts.push({type:'text',text});state.attachments.forEach(a=>parts.push(a.part));const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};addMessage(userMsg);el.composer.trigger('sune:send',{detail:{message:userMsg}});if(state.attachments.length)addAttachmentTree('user',state.attachments);if(shouldGenTitle)(async()=>{th.title=titleFrom(await generateTitleWithAI(state.messages)||partsToText(state.messages.find(m=>m.role==='user')?.content)||'Untitled');await tsave(threads);await renderThreads()})();if(!SUNE.model)return state.attachments=[],updateAttachBadge();state.busy=true;setBtnMode(true);const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=$(addSuneBubbleStreaming(suneMeta,streamId)).data('mid',streamId),assistantMsg={id:streamId,role:'assistant',content:[{type:'text',text:''}],...suneMeta};state.messages.push(assistantMsg);persistThread(false);state.stream={rid:streamId,bubble:suneBubble[0],meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done)=>{buf+=delta;state.stream.text=buf;renderMarkdown(suneBubble[0],buf,{enhance:false});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;setBtnMode(false);state.busy=false;enhanceCodeBlocks(suneBubble[0],true);persistThread(true);state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)persistThread(false)};await askOpenRouterStreaming(onDelta,streamId);state.attachments=[];updateAttachBadge()})
|
||||
let jars={html:null,extension:null},openedHTML=false;const ensureJars=async()=>{if(jars.html&&jars.extension)return jars;const mod=await import('https://medv.io/codejar/codejar.js'),CodeJar=mod.CodeJar||mod.default,hl=e=>e.innerHTML=hljs.highlight(e.textContent,{language:'xml'}).value;if(!jars.html)jars.html=CodeJar(el.htmlEditor[0],hl,{tab:' '});if(!jars.extension)jars.extension=CodeJar(el.extensionHtmlEditor[0],hl,{tab:' '});return jars}
|
||||
const openSettings=()=>{const a=SUNE.active,s=a.settings;openedHTML=false;el.suneURL.val(a.url||'');el.set_model.val(s.model);el.set_temperature.val(s.temperature);el.set_top_p.val(s.top_p);el.set_top_k.val(s.top_k);el.set_frequency_penalty.val(s.frequency_penalty);el.set_repetition_penalty.val(s.repetition_penalty);el.set_min_p.val(s.min_p);el.set_top_a.val(s.top_a);el.set_verbosity.val(s.verbosity||'');el.set_reasoning_effort.val(s.reasoning_effort||'default');el.set_system_prompt.val(s.system_prompt);el.set_hide_composer.prop('checked',!!s.hide_composer);showTab('Model');el.suneModal.removeClass('hidden')},closeSettings=()=>el.suneModal.addClass('hidden')
|
||||
const tabs={Model:['tabModel','panelModel'],Prompt:['tabPrompt','panelPrompt'],Script:['tabScript','panelScript']};function showTab(key){Object.entries(tabs).forEach(([k,[tb,pn]])=>{el[tb].toggleClass('border-black',k===key);el[pn].toggleClass('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.on('click',openSettings);el.cancelSettings.on('click',closeSettings);el.suneModal.on('click',e=>{if(e.target===el.suneModal[0]||$(e.target).hasClass('bg-black/30'))closeSettings()})
|
||||
el.tabModel.on('click',()=>showTab('Model'));el.tabPrompt.on('click',()=>showTab('Prompt'));el.tabScript.on('click',()=>showTab('Script'))
|
||||
el.settingsForm.on('submit',async e=>{e.preventDefault();SUNE.url=(el.suneURL.val()||'').trim();SUNE.model=(el.set_model.val()||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a'].forEach(k=>SUNE[k]=el[`set_${k}`].val().trim());SUNE.verbosity=(el.set_verbosity.val()||'');SUNE.reasoning_effort=(el.set_reasoning_effort.val()||'default');SUNE.system_prompt=el.set_system_prompt.val().trim();SUNE.hide_composer=el.set_hide_composer.prop('checked');if(openedHTML){SUNE.html=el.htmlEditor.text();SUNE.extension_html=el.extensionHtmlEditor.text()}closeSettings();await reflectActiveSune()})
|
||||
el.deleteSuneBtn.on('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.on('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();el.sidebarLeft.addClass('-translate-x-full');el.sidebarOverlayLeft.addClass('hidden')})
|
||||
const dl=(n,o)=>{const u=URL.createObjectURL(new Blob([JSON.stringify(o,null,2)],{type:n.endsWith('.sune')?'application/octet-stream':'application/json'})),a=$('<a>').attr({href:u,download:n}).appendTo('body');a[0].click();a.remove();URL.revokeObjectURL(u)},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.on('click',()=>{dl(`sunes-${ts()}.sune`,{version:1,sunes:SUNE.list,activeId:SUNE.id});el.userMenu.addClass('hidden')})
|
||||
el.sunesImportOption.on('click',()=>{importMode='sunes';el.importInput.val('').trigger('click')})
|
||||
el.threadsExportOption.on('click',()=>{dl(`threads-${ts()}.json`,{version:1,threads});el.userMenu.addClass('hidden')})
|
||||
el.threadsImportOption.on('click',()=>{importMode='threads';el.importInput.val('').trigger('click')})
|
||||
el.importInput.on('change',async function(){const file=this.files?.[0];if(!file)return;try{const data=JSON.parse(await file.text());if(importMode==='sunes'){const list=(Array.isArray(data)?data:(Array.isArray(data.sunes)?data.sunes:[])).map(a=>makeSune(a||{})),map={};list.forEach(s=>{if(!s.id)s.id=gid();const k=s.id,p=map[k];map[k]=!p||(+s.updatedAt>+p.updatedAt)?s:p});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 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={};(Array.isArray(data)?data:(Array.isArray(data.threads)?data.threads:[])).forEach(t=>{const n=norm(t),k=n.id,p=best[k];best[k]=!p||(+n.updatedAt>+p.updatedAt)?n:p});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.addClass('hidden')}catch{alert('Import failed')}finally{importMode=null}})
|
||||
const kbUpdate=()=>{const vv=window.visualViewport,overlap=vv?Math.max(0,(window.innerHeight-(vv.height+vv.offsetTop))):0,fh=el.footer.height();$('html').css('--kb',overlap+'px');$('html').css('--footer-h',fh+'px');el.footer.css('transform',`translateY(${-overlap}px)`);el.chat.css('scrollPaddingBottom',`${fh+overlap+16}px`)}
|
||||
const kbBind=()=>{if(window.visualViewport)['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,kbUpdate,{passive:true}));['resize','orientationchange'].forEach(ev=>$(window).on(ev,()=>setTimeout(kbUpdate,50)));['focus','click'].forEach(ev=>el.input.on(ev,()=>{setTimeout(()=>{kbUpdate();el.input[0].scrollIntoView({block:'nearest',behavior:'smooth'})},0)}))}
|
||||
const activeMeta=()=>({sune_name:SUNE.name,model:SUNE.model,avatar:SUNE.avatar});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}}
|
||||
window.handleGlobalClick=e=>{const t=$(e.target);if(t.closest('button').length)haptic();if(!el.threadPopover.has(e.target).length&&!t.closest('[data-thread-menu]').length)hideThreadPopover();if(!el.sunePopover.has(e.target).length&&!t.closest('[data-sune-menu]').length)hideSunePopover();if(!el.userMenu.has(e.target).length&&!el.userMenuBtn.has(e.target).length)el.userMenu.addClass('hidden')}
|
||||
async function init(){await SUNE.fetchDotSune('sune-org/store@main/marketplace.sune');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';
|
||||
el.htmlTab_index.onclick=()=>showHtmlTab('index');el.htmlTab_extension.onclick=()=>showHtmlTab('extension');
|
||||
init()
|
||||
const HTTP_BASE='https://orp.awww.workers.dev/ws'
|
||||
const buildBody=()=>{const msgs=[];if(SUNE.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:SUNE.masterPrompt}]});if(SUNE.system_prompt)msgs.push({role:'system',content:[{type:'text',text:SUNE.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:SUNE.model.replace(/^(or:|oai:|g:)/,''),messages:msgs,stream:true});if(SUNE.reasoning_effort&&SUNE.reasoning_effort!=='default')b.reasoning={effort:SUNE.reasoning_effort};if(SUNE.verbosity)b.verbosity=SUNE.verbosity;return b}
|
||||
$(window).on('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].toggleClass('border-black',a).toggleClass('border-transparent',!a).toggleClass('hover:border-gray-300',!a);el[pn].toggleClass('hidden',!a)})}
|
||||
el.htmlTab_index.text('index.html').on('click',()=>showHtmlTab('index'));el.htmlTab_extension.text('extension.html').on('click',()=>showHtmlTab('extension'));init()
|
||||
const HTTP_BASE='https://orp.awww.workers.dev/ws',buildBody=()=>{const msgs=[];if(SUNE.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:SUNE.masterPrompt}]});if(SUNE.system_prompt)msgs.push({role:'system',content:[{type:'text',text:SUNE.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:SUNE.model.replace(/^(or:|oai:|g:)/,''),messages:msgs,stream:true});if(SUNE.reasoning_effort&&SUNE.reasoning_effort!=='default')b.reasoning={effort:SUNE.reasoning_effort};if(SUNE.verbosity)b.verbosity=SUNE.verbosity;return b}
|
||||
async function askOpenRouterStreaming(onDelta,streamId){const model=SUNE.model,provider=model.startsWith('oai:')?'openai':model.startsWith('g:')?'google':model.startsWith('or:')?'openrouter':SUNE.provider,apiKey=provider==='openai'?SUNE.apiKeyOAI:provider==='google'?SUNE.apiKeyG:SUNE.apiKeyOR;if(!apiKey){onDelta(localDemoReply(),true);return {ok:true,rid:streamId||null}}const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null};await cacheStore.setItem(r.rid,'busy');const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const ws=new WebSocket(HTTP_BASE.replace('https','wss')+'?uid='+encodeURIComponent(r.rid));r.ws=ws;ws.onopen=()=>ws.send(JSON.stringify({type:'begin',rid:r.rid,provider,apiKey,or_body:buildBody()}));ws.onmessage=e=>{let m;try{m=JSON.parse(e.data)}catch{return}if(m.type==='delta'&&typeof m.seq==='number'&&m.seq>r.seq){r.seq=m.seq;onDelta(m.text||'',false)}else if(m.type==='done'||m.type==='err'){r.done=true;cacheStore.setItem(r.rid,'done');signal(m.type==='err'?'\n\n'+(m.message||'error'):'');ws.close()}};ws.onclose=()=>{};ws.onerror=()=>{};state.controller={abort:()=>{r.done=true;cacheStore.setItem(r.rid,'done');try{if(ws.readyState===1)ws.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{};signal('')},disconnect:()=>ws.close()};return {ok:true,rid:r.rid}}
|
||||
const accountTabs={General:['accountTabGeneral','accountPanelGeneral'],API:['accountTabAPI','accountPanelAPI']};function showAccountTab(key){Object.entries(accountTabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)})}
|
||||
function openAccountSettings(){el.set_provider.value=SUNE.provider||'openrouter';el.set_api_key_or.value=SUNE.apiKeyOR||'';el.set_api_key_oai.value=SUNE.apiKeyOAI||'';el.set_api_key_g.value=SUNE.apiKeyG||'';el.set_master_prompt.value=SUNE.masterPrompt||'';el.set_title_model.value=SUNE.titleModel;el.set_gh_token.value=globalStore.ghToken||'';showAccountTab('General');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();SUNE.provider=el.set_provider.value||'openrouter';SUNE.apiKeyOR=String(el.set_api_key_or.value||'').trim();SUNE.apiKeyOAI=String(el.set_api_key_oai.value||'').trim();SUNE.apiKeyG=String(el.set_api_key_g.value||'').trim();SUNE.masterPrompt=String(el.set_master_prompt.value||'').trim();SUNE.titleModel=String(el.set_title_model.value||'').trim();globalStore.ghToken=String(el.set_gh_token.value||'').trim();closeAccountSettings()})
|
||||
el.accountTabGeneral.onclick=()=>showAccountTab('General');el.accountTabAPI.onclick=()=>showAccountTab('API');
|
||||
el.exportAccountSettings.onclick=()=>dl(`sune-account-${ts()}.json`,{v:1,provider:globalStore.provider,apiKeyOR:globalStore.apiKeyOR,apiKeyOAI:globalStore.apiKeyOAI,apiKeyG:globalStore.apiKeyG,masterPrompt:globalStore.masterPrompt,titleModel:globalStore.titleModel,ghToken:globalStore.ghToken});
|
||||
el.importAccountSettings.onclick=()=>{el.importAccountSettingsInput.value='';el.importAccountSettingsInput.click()};
|
||||
el.importAccountSettingsInput.onchange=async e=>{const f=e.target.files?.[0];if(!f)return;try{const d=JSON.parse(await f.text());if(!d||typeof d!=='object')throw new Error('Invalid');Object.entries({provider:'string',apiKeyOR:'string',apiKeyOAI:'string',apiKeyG:'string',masterPrompt:'string',titleModel:'string',ghToken:'string'}).forEach(([k,t])=>{if(typeof d[k]===t)k==='ghToken'?globalStore[k]=d[k]:SUNE[k]=d[k]});openAccountSettings();alert('Imported.')}catch{alert('Import failed')}};
|
||||
const lastAssistantId=()=>{const a=[...el.messages.querySelectorAll('.msg-bubble')];for(let i=a.length-1;i>=0;i--){const b=a[i],h=b.previousElementSibling;if(!h)continue;const you=/^\s*You\b/.test(h.textContent||'');if(!you)return b.dataset.mid||null}return null}
|
||||
const getBubbleById=id=>el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`)
|
||||
async function syncActiveThread(){const id=lastAssistantId();if(!id)return false;if(await cacheStore.getItem(id)==='done'){if(state.busy){setBtnSend();state.busy=false;state.controller=null}return false}if(!state.busy){state.busy=true;state.controller={abort:()=>{const ws=new WebSocket(HTTP_BASE.replace('https','wss'));ws.onopen=function(){this.send(JSON.stringify({type:'stop',rid:id}));this.close()}}};setBtnStop()}const bubble=getBubbleById(id);if(!bubble)return false;const prevText=bubble.textContent||'';const j=await(fetch(HTTP_BASE+'?uid='+encodeURIComponent(id)).then(r=>r.ok?r.json():null).catch(()=>null));const finalise=(t,c)=>{renderMarkdown(bubble,t,{enhance:false});enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(x=>x.id===id);if(i>=0)state.messages[i].content=c;else state.messages.push({id,role:'assistant',content:c,...activeMeta()});persistThread();setBtnSend();state.busy=false;cacheStore.setItem(id,'done');state.controller=null};if(!j||j.rid!==id){if(j&&j.error){const t=prevText+'\n\n'+j.error;finalise(t,[{type:'text',text:t}])}return false}const text=j.text||'',isDone=j.error||j.done||j.phase==='done';if(text)renderMarkdown(bubble,text,{enhance:false});if(isDone){const finalText=text||prevText;finalise(finalText,[{type:'text',text:finalText}]);return false}await cacheStore.setItem(id,'busy');return true}
|
||||
let syncLoopRunning=false
|
||||
async function syncWhileBusy(){if(syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=true;try{while(await syncActiveThread())await new Promise(r=>setTimeout(r,1200))}finally{syncLoopRunning=false}}
|
||||
const onForeground=()=>{if(document.visibilityState!=='visible')return;state.controller?.disconnect?.();if(state.busy)syncWhileBusy()}
|
||||
document.addEventListener('visibilitychange',onForeground)
|
||||
el.copySystemPrompt.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}})
|
||||
el.pasteSystemPrompt.addEventListener('click',async()=>{try{el.set_system_prompt.value=await navigator.clipboard.readText()}catch{}})
|
||||
const getActiveHtmlParts=()=>!el.htmlEditor.classList.contains('hidden')?[el.htmlEditor,jars.html]:[el.extensionHtmlEditor,jars.extension]
|
||||
el.copyHTML.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent||'')}catch{}})
|
||||
el.pasteHTML.addEventListener('click',async()=>{try{const t=await navigator.clipboard.readText();const[editor,jar]=getActiveHtmlParts();if(jar&&jar.updateCode)jar.updateCode(t);else if(editor)editor.textContent=t}catch{}})
|
||||
Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,enhanceCodeBlocks,getSuneLabel,msgRow,partsToText,addSuneBubbleStreaming,clearChat,payloadWithSampling,setBtnStop,setBtnSend,localDemoReply,titleFrom,tload,tsave,ensureThreadOnFirstUser,generateTitleWithAI,persistThread,threadRow,renderThreads,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,updateAttachBadge,toAttach,attachmentsText,addAttachmentTree,ensureJars,openSettings,closeSettings,showTab,dl,ts,kbUpdate,kbBind,activeMeta,init,showHtmlTab,buildBody,askOpenRouterStreaming,showAccountTab,openAccountSettings,closeAccountSettings,lastAssistantId,getBubbleById,syncActiveThread,syncWhileBusy,onForeground,getActiveHtmlParts});
|
||||
const accountTabs={General:['accountTabGeneral','accountPanelGeneral'],API:['accountTabAPI','accountPanelAPI']};function showAccountTab(key){Object.entries(accountTabs).forEach(([k,[tb,pn]])=>{el[tb].toggleClass('border-black',k===key);el[pn].toggleClass('hidden',k!==key)})}
|
||||
function openAccountSettings(){el.set_provider.val(SUNE.provider||'openrouter');el.set_api_key_or.val(SUNE.apiKeyOR||'');el.set_api_key_oai.val(SUNE.apiKeyOAI||'');el.set_api_key_g.val(SUNE.apiKeyG||'');el.set_master_prompt.val(SUNE.masterPrompt||'');el.set_title_model.val(SUNE.titleModel);el.set_gh_token.val(globalStore.ghToken||'');showAccountTab('General');el.accountSettingsModal.removeClass('hidden')}
|
||||
function closeAccountSettings(){el.accountSettingsModal.addClass('hidden')}
|
||||
el.accountSettingsOption.on('click',()=>{el.userMenu.addClass('hidden');openAccountSettings()});el.closeAccountSettings.on('click',closeAccountSettings);el.cancelAccountSettings.on('click',closeAccountSettings);el.accountSettingsModal.on('click',e=>{if(e.target===el.accountSettingsModal[0]||$(e.target).hasClass('bg-black/30'))closeAccountSettings()})
|
||||
el.accountSettingsForm.on('submit',e=>{e.preventDefault();SUNE.provider=el.set_provider.val()||'openrouter';SUNE.apiKeyOR=String(el.set_api_key_or.val()||'').trim();SUNE.apiKeyOAI=String(el.set_api_key_oai.val()||'').trim();SUNE.apiKeyG=String(el.set_api_key_g.val()||'').trim();SUNE.masterPrompt=String(el.set_master_prompt.val()||'').trim();SUNE.titleModel=String(el.set_title_model.val()||'').trim();globalStore.ghToken=String(el.set_gh_token.val()||'').trim();closeAccountSettings()})
|
||||
el.accountTabGeneral.on('click',()=>showAccountTab('General'));el.accountTabAPI.on('click',()=>showAccountTab('API'));
|
||||
el.exportAccountSettings.on('click',()=>dl(`sune-account-${ts()}.json`,{v:1,provider:globalStore.provider,apiKeyOR:globalStore.apiKeyOR,apiKeyOAI:globalStore.apiKeyOAI,apiKeyG:globalStore.apiKeyG,masterPrompt:globalStore.masterPrompt,titleModel:globalStore.titleModel,ghToken:globalStore.ghToken}));
|
||||
el.importAccountSettings.on('click',()=>el.importAccountSettingsInput.val('').trigger('click'));
|
||||
el.importAccountSettingsInput.on('change',async function(){const f=this.files?.[0];if(!f)return;try{const d=JSON.parse(await f.text());if(!d||typeof d!=='object')throw new Error('Invalid');Object.entries({provider:'string',apiKeyOR:'string',apiKeyOAI:'string',apiKeyG:'string',masterPrompt:'string',titleModel:'string',ghToken:'string'}).forEach(([k,t])=>{if(typeof d[k]===t)k==='ghToken'?globalStore[k]=d[k]:SUNE[k]=d[k]});openAccountSettings();alert('Imported.')}catch{alert('Import failed')}});
|
||||
const lastAssistantId=()=>{const a=[...el.messages.find('.msg-bubble')].reverse();for(const b of a){const h=$(b).prev();if(h.length&&!/^\s*You\b/.test(h.text()||''))return $(b).data('mid')||null}return null},getBubbleById=id=>el.messages.find(`.msg-bubble[data-mid="${CSS.escape(id)}"]`)
|
||||
async function syncActiveThread(){const id=lastAssistantId();if(!id)return false;if(await cacheStore.getItem(id)==='done'){if(state.busy){setBtnMode(false);state.busy=false;state.controller=null}return false}if(!state.busy){state.busy=true;state.controller={abort:()=>{const ws=new WebSocket(HTTP_BASE.replace('https','wss'));ws.onopen=function(){this.send(JSON.stringify({type:'stop',rid:id}));this.close()}}};setBtnMode(true)}const bubble=getBubbleById(id);if(!bubble.length)return false;const prevText=bubble.text()||'';const j=await(fetch(HTTP_BASE+'?uid='+encodeURIComponent(id)).then(r=>r.ok?r.json():null).catch(()=>null));const finalise=(t,c)=>{renderMarkdown(bubble[0],t,{enhance:false});enhanceCodeBlocks(bubble[0],true);const i=state.messages.findIndex(x=>x.id===id);if(i>=0)state.messages[i].content=c;else state.messages.push({id,role:'assistant',content:c,...activeMeta()});persistThread();setBtnMode(false);state.busy=false;cacheStore.setItem(id,'done');state.controller=null};if(!j||j.rid!==id){if(j&&j.error){const t=prevText+'\n\n'+j.error;finalise(t,[{type:'text',text:t}])}return false}const text=j.text||'',isDone=j.error||j.done||j.phase==='done';if(text)renderMarkdown(bubble[0],text,{enhance:false});if(isDone){const finalText=text||prevText;finalise(finalText,[{type:'text',text:finalText}]);return false}await cacheStore.setItem(id,'busy');return true}
|
||||
let syncLoopRunning=false;async function syncWhileBusy(){if(syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=true;try{while(await syncActiveThread())await new Promise(r=>setTimeout(r,1200))}finally{syncLoopRunning=false}}
|
||||
const onForeground=()=>{if(document.visibilityState!=='visible')return;state.controller?.disconnect?.();if(state.busy)syncWhileBusy()};$(document).on('visibilitychange',onForeground)
|
||||
el.copySystemPrompt.on('click',async()=>await navigator.clipboard.writeText(el.set_system_prompt.val()||'').catch(()=>{}));el.pasteSystemPrompt.on('click',async()=>el.set_system_prompt.val(await navigator.clipboard.readText().catch(()=>'')))
|
||||
const getActiveHtmlParts=()=>!el.htmlEditor.hasClass('hidden')?[el.htmlEditor,jars.html]:[el.extensionHtmlEditor,jars.extension]
|
||||
el.copyHTML.on('click',async()=>await navigator.clipboard.writeText(getActiveHtmlParts()[0].text()||'').catch(()=>{}));el.pasteHTML.on('click',async()=>{try{const t=await navigator.clipboard.readText(),[editor,jar]=getActiveHtmlParts();if(jar&&jar.updateCode)jar.updateCode(t);else if(editor)editor.text(t)}catch{}})
|
||||
Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,enhanceCodeBlocks,getSuneLabel,msgRow,partsToText,addSuneBubbleStreaming,clearChat,payloadWithSampling,setBtnMode,titleFrom,tload,tsave,ensureThreadOnFirstUser,generateTitleWithAI,persistThread,threadRow,renderThreads,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,updateAttachBadge,toAttach,attachmentsText,addAttachmentTree,ensureJars,openSettings,closeSettings,showTab,dl,ts,kbUpdate,kbBind,activeMeta,init,showHtmlTab,buildBody,askOpenRouterStreaming,showAccountTab,openAccountSettings,closeAccountSettings,lastAssistantId,getBubbleById,syncActiveThread,syncWhileBusy,onForeground,getActiveHtmlParts});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2
dist/sw.js
vendored
2
dist/sw.js
vendored
@@ -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} didn’t 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:"25141f5c67f5c96dd3549e25a990c0ab"},{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} didn’t 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:"66e87da7965399515e5af61bf179b1ca"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
||||
|
||||
Reference in New Issue
Block a user