This build was committed by a bot.

This commit is contained in:
github-actions
2025-08-27 19:02:48 +00:00
parent ed858e6954
commit e85de4f4bb
4 changed files with 41 additions and 25 deletions

50
dist/index.html vendored
View File

@@ -30,7 +30,7 @@
<div class="justify-self-end"><button id="historyBtn" 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:renderHistory();document.getElementById('historyPanel').classList.remove('translate-x-full');document.getElementById('historyOverlay').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div> <div class="justify-self-end"><button id="historyBtn" 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:renderHistory();document.getElementById('historyPanel').classList.remove('translate-x-full');document.getElementById('historyOverlay').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
</div> </div>
</header> </header>
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar"><section id="suneHtml" class="px-4 py-3 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('sidebar').classList.remove('-translate-x-full');document.getElementById('sidebarOverlay').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 py-3 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('sidebar').classList.remove('-translate-x-full');document.getElementById('sidebarOverlay').classList.remove('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"> <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"> <div class="mx-auto w-full max-w-none px-0">
<form id="composer" class="group relative flex items-start gap-2 px-3"> <form id="composer" class="group relative flex items-start gap-2 px-3">
@@ -84,7 +84,7 @@
<form id="settingsForm" class="text-sm"> <form id="settingsForm" class="text-sm">
<div class="border-b flex text-xs font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">Script</button></div> <div class="border-b flex text-xs font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">Script</button></div>
<div id="panelModel" class="p-4 space-y-4"> <div id="panelModel" class="p-4 space-y-4">
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="openai/gpt-4o"/><p class="mt-1 text-xs text-gray-500">Find on OpenRouter.</p></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select><p class="mt-1 text-xs text-gray-500">If supported by the model.</p></div></div> <div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="openai/gpt-5"/><p class="mt-1 text-xs text-gray-500">Optional: prefix with or: or oai:</p></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select><p class="mt-1 text-xs text-gray-500">If supported by the model.</p></div></div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(02)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Variety. Lower = predictable.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(02)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Variety. Lower = predictable.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(01)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Nucleus sampling.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(01)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Nucleus sampling.</p></div>
@@ -122,37 +122,48 @@
<div class="absolute inset-x-0 top-16 mx-auto w-full max-w-md px-4"> <div class="absolute inset-x-0 top-16 mx-auto w-full max-w-md px-4">
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden"> <div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Account Settings</span><button id="closeAccountSettings" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div> <div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Account Settings</span><button id="closeAccountSettings" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
<form id="accountSettingsForm" class="p-4 text-sm space-y-4"> <form id="accountSettingsForm" class="text-sm">
<div class="border-b flex text-xs font-medium"><button type="button" id="accountTabGeneral" class="flex-1 py-2 px-3 text-center border-b-2 border-black">General</button><button type="button" id="accountTabAPI" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">API</button></div>
<div id="accountPanelGeneral" class="p-4 space-y-4">
<div><label class="block text-gray-700 font-medium mb-1">Provider</label><select id="set_provider" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="openrouter">OpenRouter</option><option value="openai">OpenAI</option></select><p class="mt-1 text-xs text-gray-500">Used for streaming.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Provider</label><select id="set_provider" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="openrouter">OpenRouter</option><option value="openai">OpenAI</option></select><p class="mt-1 text-xs text-gray-500">Used for streaming.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
</div>
<div id="accountPanelAPI" class="p-4 space-y-4 hidden">
<div><label class="block text-gray-700 font-medium mb-1">OpenRouter API Key</label><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-or-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div> <div><label class="block text-gray-700 font-medium mb-1">OpenRouter API Key</label><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-or-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">OpenAI API Key</label><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div> <div><label class="block text-gray-700 font-medium mb-1">OpenAI API Key</label><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Github Token</label><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="ghp_... (optional)"/><p class="mt-1 text-xs text-gray-500">Optional. For future use.</p></div>
<div class="flex items-center justify-end gap-2 pt-2 border-t"><button type="button" id="cancelAccountSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div> </div>
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
<div class="flex items-center gap-2"><button type="button" id="importAccountSettings" class="text-xs px-2.5 py-1.5 rounded-lg border bg-white hover:bg-gray-50">Import</button><button type="button" id="exportAccountSettings" class="text-xs px-2.5 py-1.5 rounded-lg border bg-white hover:bg-gray-50">Export</button></div>
<div class="flex items-center justify-end gap-2"><button type="button" id="cancelAccountSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
</div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<input id="importAccountSettingsInput" type="file" class="hidden" accept="application/json,.json">
<script src="https://unpkg.com/lucide@latest"></script> <script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
<script> <script>
const DEFAULT_MODEL='openai/gpt-5',DEFAULT_API_KEY='' const DEFAULT_MODEL='openai/gpt-5',DEFAULT_API_KEY=''
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript','panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_presence_penalty','set_repetition_penalty','set_min_p','set_top_a','set_max_tokens','set_verbosity','set_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','accountSettingsOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput','scriptEditor','htmlEditor','subTabHTML','subTabJS','panelHTML','panelJS','suneHtml','accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','set_master_prompt','set_provider','set_api_key_or','set_api_key_oai','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','copyJS','pasteJS'].map(id=>[id,document.getElementById(id)])) const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript','panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_presence_penalty','set_repetition_penalty','set_min_p','set_top_a','set_max_tokens','set_verbosity','set_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','accountSettingsOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput','scriptEditor','htmlEditor','subTabHTML','subTabJS','panelHTML','panelJS','suneHtml','accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','set_master_prompt','set_provider','set_api_key_or','set_api_key_oai','set_title_model','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','copyJS','pasteJS','accountTabGeneral','accountTabAPI','accountPanelGeneral','accountPanelAPI','set_gh_token','importAccountSettings','exportAccountSettings','importAccountSettingsInput'].map(id=>[id,document.getElementById(id)]))
const icons=()=>window.lucide&&lucide.createIcons() const icons=()=>window.lucide&&lucide.createIcons()
const clamp=(v,min,max)=>Math.max(min,Math.min(max,v)),num=(v,d)=>v==null||v===''||isNaN(+v)?d:+v,int=(v,d)=>v==null||v===''||isNaN(parseInt(v))?d:parseInt(v),gid=()=>Math.random().toString(36).slice(2,9),esc=s=>String(s).replace(/[&<>'"`]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;","`":"&#96;"}[c])) const clamp=(v,min,max)=>Math.max(min,Math.min(max,v)),num=(v,d)=>v==null||v===''||isNaN(+v)?d:+v,int=(v,d)=>v==null||v===''||isNaN(parseInt(v))?d:parseInt(v),gid=()=>Math.random().toString(36).slice(2,9),esc=s=>String(s).replace(/[&<>'"`]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;","`":"&#96;"}[c]))
const sid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6) const 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 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 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 b64=x=>x.split(',')[1]||''
const globalStore={get provider(){return localStorage.getItem('provider')||'openrouter'},set provider(v){localStorage.setItem('provider',v==='openai'?'openai':'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 masterPrompt(){return localStorage.getItem('master_prompt')||''},set masterPrompt(v){localStorage.setItem('master_prompt',v||'')}} const globalStore={get provider(){return localStorage.getItem('provider')||'openrouter'},set provider(v){localStorage.setItem('provider',v==='openai'?'openai':'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 masterPrompt(){return localStorage.getItem('master_prompt')||''},set masterPrompt(v){localStorage.setItem('master_prompt',v||'')},get titleModel(){return localStorage.getItem('title_model')??'or:meta-llama/llama-3.2-3b-instruct'},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 su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}}
const defaultSettings={model:DEFAULT_MODEL,temperature:1,top_p:0.97,top_k:0,frequency_penalty:0,presence_penalty:0,repetition_penalty:1,min_p:0,top_a:0,max_tokens:0,verbosity:'',reasoning_effort:'default',system_prompt:'',script:'',html:''} const defaultSettings={model:DEFAULT_MODEL,temperature:1,top_p:0.97,top_k:0,frequency_penalty:0,presence_penalty:0,repetition_penalty:1,min_p:0,top_a:0,max_tokens:0,verbosity:'',reasoning_effort:'default',system_prompt:'',script:'',html:''}
const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{})}) const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{})})
let sunes=(su.load()||[]).map(makeSune) let sunes=(su.load()||[]).map(makeSune)
if(!sunes.length){const def=makeSune({name:'Default'});sunes=[def];su.save(sunes);su.setActiveId(def.id)} if(!sunes.length){const def=makeSune({name:'Default'});sunes=[def];su.save(sunes);su.setActiveId(def.id)}
const getActiveSune=()=>sunes.find(a=>a.id===su.getActiveId())||sunes[0],createDefaultSune=()=>makeSune({name:'Default'}) const getActiveSune=()=>sunes.find(a=>a.id===su.getActiveId())||sunes[0],createDefaultSune=()=>makeSune({name:'Default'})
const store=new Proxy({},{get(_,p){if(p==='provider')return globalStore.provider;if(p==='apiKey')return globalStore.provider==='openai'?globalStore.apiKeyOAI:globalStore.apiKeyOR;if(p==='apiKeyOR')return globalStore.apiKeyOR;if(p==='apiKeyOAI')return globalStore.apiKeyOAI;if(p==='masterPrompt')return globalStore.masterPrompt;const a=getActiveSune();if(p==='model')return a.settings.model;if(p in a.settings)return a.settings[p];if(p==='system_prompt')return a.settings.system_prompt},set(_,p,v){if(p==='provider'){globalStore.provider=v;return true}if(p==='apiKey'){if(globalStore.provider==='openai')globalStore.apiKeyOAI=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==='masterPrompt'){globalStore.masterPrompt=v;return true}const i=sunes.findIndex(a=>a.id===getActiveSune().id);if(i>=0){if(p==='model')sunes[i].settings.model=v||DEFAULT_MODEL;else if(p==='system_prompt')sunes[i].settings.system_prompt=v||'';else sunes[i].settings[p]=v;sunes[i].updatedAt=Date.now();su.save(sunes);return true}return false}}) const store=new Proxy({},{get(_,p){if(p==='provider')return globalStore.provider;if(p==='apiKey')return globalStore.provider==='openai'?globalStore.apiKeyOAI:globalStore.apiKeyOR;if(p==='apiKeyOR')return globalStore.apiKeyOR;if(p==='apiKeyOAI')return globalStore.apiKeyOAI;if(p==='masterPrompt')return globalStore.masterPrompt;if(p==='titleModel')return globalStore.titleModel;const a=getActiveSune();if(p==='model')return a.settings.model;if(p in a.settings)return a.settings[p];if(p==='system_prompt')return a.settings.system_prompt},set(_,p,v){if(p==='provider'){globalStore.provider=v;return true}if(p==='apiKey'){if(globalStore.provider==='openai')globalStore.apiKeyOAI=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==='masterPrompt'){globalStore.masterPrompt=v;return true}if(p==='titleModel'){globalStore.titleModel=v;return true}const i=sunes.findIndex(a=>a.id===getActiveSune().id);if(i>=0){if(p==='model')sunes[i].settings.model=v||DEFAULT_MODEL;else if(p==='system_prompt')sunes[i].settings.system_prompt=v||'';else sunes[i].settings[p]=v;sunes[i].updatedAt=Date.now();su.save(sunes);return true}return false}})
const state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:false}} const state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:false}}
const getModelShort=m=>{const mm=m||store.model||'';return mm.includes('/')?mm.split('/').pop():mm} const getModelShort=m=>{const mm=m||store.model||'';return mm.includes('/')?mm.split('/').pop():mm}
const renderSuneHTML=()=>{const m=el.suneHtml,h=(getActiveSune().settings.html||'').trim();m.innerHTML='';m.classList.toggle('hidden',!h);if(!h)return;m.insertAdjacentHTML('afterbegin',h);m.querySelectorAll('script').forEach(s=>{const n=document.createElement('script');[...s.attributes].forEach(a=>n.setAttribute(a.name,a.value));n.text=s.text; s.replaceWith(n)})} const renderSuneHTML=()=>{const m=el.suneHtml,h=(getActiveSune().settings.html||'').trim();m.innerHTML='';m.classList.toggle('hidden',!h);if(!h)return;m.insertAdjacentHTML('afterbegin',h);m.querySelectorAll('script').forEach(s=>{const n=document.createElement('script');[...s.attributes].forEach(a=>n.setAttribute(a.name,a.value));n.text=s.text; s.replaceWith(n)})}
@@ -176,7 +187,7 @@ let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)
const TKEY='threads_v1',tload=()=>localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[]),tsave=v=>localforage.setItem(TKEY,v) const TKEY='threads_v1',tload=()=>localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[]),tsave=v=>localforage.setItem(TKEY,v)
const cacheStore=localforage.createInstance({name:'master_cache'}); const cacheStore=localforage.createInstance({name:'master_cache'});
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:titleFrom(text),pinned:false,updatedAt:now,messages:[]};state.currentThreadId=id;threads.unshift(th);await tsave(threads);await renderHistory()} 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:titleFrom(text),pinned:false,updatedAt:now,messages:[]};state.currentThreadId=id;threads.unshift(th);await tsave(threads);await renderHistory()}
async function persistThread(){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId);if(!th)return;th.messages=[...state.messages];th.updatedAt=Date.now();th.title=titleFrom(partsToText(th.messages.find(m=>m.role==='user')?.content)||th.title);await tsave(threads);await renderHistory()} 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();th.title=titleFrom(partsToText(th.messages.find(m=>m.role==='user')?.content)||th.title)}await tsave(threads);if(full)await renderHistory()}
const historyRow=t=>`<div class=\"relative flex items-center gap-2 px-3 py-2 ${t.pinned?'bg-yellow-50':''}\"><button data-open-thread=\"${t.id}\" class=\"flex-1 text-left truncate\">${t.pinned?'📌 ':''}${esc(t.title)}</button><button data-thread-menu=\"${t.id}\" class=\"h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center\" title=\"More\"><i data-lucide=\"more-horizontal\" class=\"h-4 w-4\"></i></button></div>` const historyRow=t=>`<div class=\"relative flex items-center gap-2 px-3 py-2 ${t.pinned?'bg-yellow-50':''}\"><button data-open-thread=\"${t.id}\" class=\"flex-1 text-left truncate\">${t.pinned?'📌 ':''}${esc(t.title)}</button><button data-thread-menu=\"${t.id}\" class=\"h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center\" title=\"More\"><i data-lucide=\"more-horizontal\" class=\"h-4 w-4\"></i></button></div>`
async function renderHistory(){const list=[...threads].sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt));el.historyList.innerHTML=list.map(historyRow).join('');icons()} async function renderHistory(){const list=[...threads].sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt));el.historyList.innerHTML=list.map(historyRow).join('');icons()}
let menuThreadId=null;const hideHistoryMenu=()=>{el.historyMenu.classList.add('hidden');menuThreadId=null} let menuThreadId=null;const hideHistoryMenu=()=>{el.historyMenu.classList.add('hidden');menuThreadId=null}
@@ -194,7 +205,7 @@ function addAttachmentTree(role,arr){if(!arr?.length)return;const id=gid(),text=
el.attachBtn.addEventListener('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.value=''};el.fileInput.click()}) el.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.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.messages.addEventListener('click',async e=>{const a=e.target.closest('a[href^="#dl-"]');if(!a)return; e.preventDefault();const m=a.getAttribute('href').match(/^#dl-([^-]+)-(\d+)$/);if(!m)return;const id=m[1],i=+m[2];const msg=state.messages.find(x=>x.id===id),meta=msg?.attachmentsMeta?.[i];if(!meta)return;let blob;if(meta.mode==='dataURL'){blob=await (await fetch(meta.data)).blob()}else{const bin=Uint8Array.from(atob(meta.data),c=>c.charCodeAt(0));blob=new Blob([bin],{type:meta.mime||'application/octet-stream'})}const url=URL.createObjectURL(blob),dl=document.createElement('a');dl.href=url;dl.download=meta.name||'download';document.body.appendChild(dl);dl.click();dl.remove();URL.revokeObjectURL(url)})
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text||'(attachments)');el.input.value='';const parts=[];if(text)parts.push({type:'text',text});state.attachments.forEach(a=>parts.push(a.part));addMessage({role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]});if(state.attachments.length)addAttachmentTree('user',state.attachments);state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);const streamId=sid();suneBubble.dataset.mid=streamId;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});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push(Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:buf}]},suneMeta));persistThread();state.stream={rid:null,bubble:null,meta:null,text:'',done:false};queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}};await askOpenRouterStreaming(onDelta,streamId);state.attachments=[];updateAttachBadge()}) el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text||'(attachments)');el.input.value='';const parts=[];if(text)parts.push({type:'text',text});state.attachments.forEach(a=>parts.push(a.part));addMessage({role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]});if(state.attachments.length)addAttachmentTree('user',state.attachments);state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);const streamId=sid();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={js:null,html:null};const ensureJars=async()=>{if(jars.js&&jars.html)return jars;const mod=await import('https://medv.io/codejar/codejar.js');const CodeJar=mod.CodeJar||mod.default;const mk=(elx,lang)=>CodeJar(elx,ed=>{ed.innerHTML=hljs.highlight(ed.textContent,{language:lang}).value},{tab:' '});if(!jars.js)jars.js=mk(el.scriptEditor,'javascript');if(!jars.html)jars.html=mk(el.htmlEditor,'xml');return jars} let jars={js:null,html:null};const ensureJars=async()=>{if(jars.js&&jars.html)return jars;const mod=await import('https://medv.io/codejar/codejar.js');const CodeJar=mod.CodeJar||mod.default;const mk=(elx,lang)=>CodeJar(elx,ed=>{ed.innerHTML=hljs.highlight(ed.textContent,{language:lang}).value},{tab:' '});if(!jars.js)jars.js=mk(el.scriptEditor,'javascript');if(!jars.html)jars.html=mk(el.htmlEditor,'xml');return jars}
let openedJS=false,openedHTML=false let openedJS=false,openedHTML=false
function openSettings(){const a=getActiveSune(),s=a.settings;openedJS=false;openedHTML=false;el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_presence_penalty.value=s.presence_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_max_tokens.value=s.max_tokens||'';el.set_verbosity.value=s.verbosity||'';el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;showTab('Model');el.settingsModal.classList.remove('hidden')} function openSettings(){const a=getActiveSune(),s=a.settings;openedJS=false;openedHTML=false;el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_presence_penalty.value=s.presence_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_max_tokens.value=s.max_tokens||'';el.set_verbosity.value=s.verbosity||'';el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;showTab('Model');el.settingsModal.classList.remove('hidden')}
@@ -231,22 +242,27 @@ async function init(){threads=await tload();await renderHistory();renderSidebar(
window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()}) window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()})
init() init()
const HTTP_BASE='https://orp.awww.workers.dev/ws' const HTTP_BASE='https://orp.awww.workers.dev/ws'
const buildBody=()=>{const msgs=[];if(store.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:store.masterPrompt}]});if(store.system_prompt)msgs.push({role:'system',content:[{type:'text',text:store.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:store.model,messages:msgs,stream:true});if(store.reasoning_effort&&store.reasoning_effort!=='default')b.reasoning={effort:store.reasoning_effort};if(store.verbosity)b.verbosity=store.verbosity;return b} const buildBody=()=>{const msgs=[];if(store.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:store.masterPrompt}]});if(store.system_prompt)msgs.push({role:'system',content:[{type:'text',text:store.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:store.model.replace(/^(or:|oai:)/,''),messages:msgs,stream:true});if(store.reasoning_effort&&store.reasoning_effort!=='default')b.reasoning={effort:store.reasoning_effort};if(store.verbosity)b.verbosity=store.verbosity;return b}
async function askOpenRouterStreaming(onDelta,streamId){if(!store.apiKey){const t=localDemoReply();onDelta(t,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:store.provider,apiKey:store.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}} async function askOpenRouterStreaming(onDelta,streamId){const model=store.model,provider=model.startsWith('oai:')?'openai':model.startsWith('or:')?'openrouter':store.provider,apiKey=provider==='openai'?store.apiKeyOAI:store.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}}
function openAccountSettings(){el.set_provider.value=store.provider||'openrouter';el.set_api_key_or.value=store.apiKeyOR||'';el.set_api_key_oai.value=store.apiKeyOAI||'';el.set_master_prompt.value=store.masterPrompt||'';el.accountSettingsModal.classList.remove('hidden')} 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=store.provider||'openrouter';el.set_api_key_or.value=store.apiKeyOR||'';el.set_api_key_oai.value=store.apiKeyOAI||'';el.set_master_prompt.value=store.masterPrompt||'';el.set_title_model.value=store.titleModel;el.set_gh_token.value=globalStore.ghToken||'';showAccountTab('General');el.accountSettingsModal.classList.remove('hidden')}
function closeAccountSettings(){el.accountSettingsModal.classList.add('hidden')} function closeAccountSettings(){el.accountSettingsModal.classList.add('hidden')}
el.accountSettingsOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()}) el.accountSettingsOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()})
el.closeAccountSettings.addEventListener('click',closeAccountSettings) el.closeAccountSettings.addEventListener('click',closeAccountSettings)
el.cancelAccountSettings.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.accountSettingsModal.addEventListener('click',e=>{if(e.target===el.accountSettingsModal||e.target.classList.contains('bg-black/30'))closeAccountSettings()})
el.accountSettingsForm.addEventListener('submit',e=>{e.preventDefault();store.provider=el.set_provider.value||'openrouter';store.apiKeyOR=String(el.set_api_key_or.value||'').trim();store.apiKeyOAI=String(el.set_api_key_oai.value||'').trim();store.masterPrompt=String(el.set_master_prompt.value||'').trim();closeAccountSettings()}) el.accountSettingsForm.addEventListener('submit',e=>{e.preventDefault();store.provider=el.set_provider.value||'openrouter';store.apiKeyOR=String(el.set_api_key_or.value||'').trim();store.apiKeyOAI=String(el.set_api_key_oai.value||'').trim();store.masterPrompt=String(el.set_master_prompt.value||'').trim();store.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,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',masterPrompt:'string',titleModel:'string',ghToken:'string'}).forEach(([k,t])=>{if(typeof d[k]===t)globalStore[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 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)}"]`) 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 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=(bubble.textContent||'')+'\n\n'+j.error;finalise(t,[{type:'text',text:t}])}return false}const text=j.text||'',isDone=j.error||j.done||j.phase==='done';renderMarkdown(bubble,text,{enhance:false});if(isDone){finalise(text,[{type:'text',text}]);return false}await cacheStore.setItem(id,'busy');return true} 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 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}} 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}}
;['focus','pageshow'].forEach(ev=>window.addEventListener(ev,syncWhileBusy)) const onForeground=()=>{if(document.visibilityState!=='visible')return;state.controller?.disconnect?.();if(state.busy)syncWhileBusy()}
document.addEventListener('visibilitychange',()=>document.visibilityState==='visible'&&syncWhileBusy()) document.addEventListener('visibilitychange',onForeground)
el.copySystemPrompt.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}}) 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{}}) el.pasteSystemPrompt.addEventListener('click',async()=>{try{el.set_system_prompt.value=await navigator.clipboard.readText()}catch{}})
el.copyHTML.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.htmlEditor.textContent||'')}catch{}}) el.copyHTML.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.htmlEditor.textContent||'')}catch{}})

2
dist/sw.js vendored
View File

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

View File

@@ -2,9 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<script src="https://cdn.tailwindcss.com"> <script src="https://cdn.tailwindcss.com"></script>
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-light.min.css"/> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-light.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"/> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"/>
<style> <style>
@@ -27,7 +25,7 @@
<div class="justify-self-end"><button id="historyBtn" 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:renderHistory();document.getElementById('historyPanel').classList.remove('translate-x-full');document.getElementById('historyOverlay').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div> <div class="justify-self-end"><button id="historyBtn" 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:renderHistory();document.getElementById('historyPanel').classList.remove('translate-x-full');document.getElementById('historyOverlay').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
</div> </div>
</header> </header>
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar"><section id="suneHtml" class="px-1 py-3 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('sidebar').classList.remove('-translate-x-full');document.getElementById('sidebarOverlay').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 py-3 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('sidebar').classList.remove('-translate-x-full');document.getElementById('sidebarOverlay').classList.remove('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"> <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"> <div class="mx-auto w-full max-w-none px-0">
<form id="composer" class="group relative flex items-start gap-2 px-3"> <form id="composer" class="group relative flex items-start gap-2 px-3">

View File

@@ -1 +1,3 @@
All Rights Reserved. All Rights Reserved.
EMAIL ME.