Feat: Create main entrypoint for JS modules

This commit is contained in:
2025-11-07 20:25:27 -08:00
parent e8039778bc
commit 0f069c9521

84
src/main.js Normal file
View File

@@ -0,0 +1,84 @@
import * as api from './api.js';
import * as utils from './utils.js';
import * as services from './services.js';
import * as ui from './ui.js';
let syncLoopRunning=false;
async function syncWhileBusy(){if(syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=true;try{while(await ui.syncActiveThread())await new Promise(r=>setTimeout(r,1500))}finally{syncLoopRunning=false}};
const SUNE_PROXY=new Proxy(api.SUNE_CORE,{
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=api.sunes.length;api.sunes.unshift(...(Array.isArray(j)?j:j?.sunes||[]).filter(s=>s?.id&&!t.get(s.id)).map(api.makeSune));api.sunes.length>l&&t.save()}catch{}};
if(p==='attach')return async files=>{const arr=[];for(const f of files||[])arr.push(await ui.toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ui.ensureThreadOnFirstUser('(attachments)');ui.addMessage({role:'assistant',content:clean,...ui.activeMeta()});await api.THREAD.persist()};
if(p==='log')return async s=>{const t=String(s??'').trim();if(!t)return;await ui.ensureThreadOnFirstUser(t);ui.addMessage({role:'assistant',content:[{type:'text',text:t}],...ui.activeMeta()});await api.THREAD.persist()};
if(p==='lastReply')return [...api.state.messages].reverse().find(m=>m.role==='assistant');
if(p==='infer')return async()=>{if(api.state.busy||!SUNE_PROXY.model||api.state.abortRequested){api.state.abortRequested=false;return};await ui.ensureThreadOnFirstUser('Sune Inference');const th=api.THREAD.active;if(th&&!th.title)(async()=>api.THREAD.setTitle(th.id,await services.generateTitleWithAI(api.state.messages)||'Sune Inference'))();api.state.busy=true;ui.setBtnStop();const a=SUNE_PROXY.active,suneMeta={sune_name:a.name,model:SUNE_PROXY.model,avatar:a.avatar||''},streamId=utils.sid(),suneBubble=ui.addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);api.state.messages.push(assistantMsg);api.THREAD.persist(false);api.state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done)=>{buf+=delta;api.state.stream.text=buf;ui.renderMarkdown(suneBubble,buf,{enhance:false});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;ui.setBtnSend();api.state.busy=false;ui.enhanceCodeBlocks(suneBubble,true);api.THREAD.persist(true);ui.el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));api.state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)api.THREAD.persist(false)};await services.askOpenRouterStreaming(onDelta,streamId)};
if(p==='handoff')return async n=>{await new Promise(r=>setTimeout(r,4000));const s=api.sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(!s)return;api.SUNE_CORE.setActive(s.id);ui.renderSidebar();await ui.reflectActiveSune();await SUNE_PROXY.infer()};
if(p in t)return t[p];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){const a=t.active;if(!a)return false;const i=api.sunes.findIndex(s=>s.id===a.id);if(i<0)return false;const isTopLevel=/^(name|avatar|url|pinned|storage)$/.test(p),target=isTopLevel?api.sunes[i]:api.sunes[i].settings;let value=v;if(!isTopLevel){if(p==='system_prompt')value=v||''}if(target[p]!==value){target[p]=value;api.sunes[i].updatedAt=Date.now();t.save()}return true}
});
function bindEvents(){
const {el}=ui;
$(el.composer).on('submit',async e=>{e.preventDefault();if(api.state.busy)return;const text=el.input.value.trim();if(!text&&!api.state.attachments.length)return SUNE_PROXY.infer();await ui.ensureThreadOnFirstUser(text||'(attachments)');const th=api.THREAD.active,shouldGenTitle=th&&!th.title;el.input.value='';const parts=[];if(text)parts.push({type:'text',text});parts.push(...api.state.attachments);const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};ui.addMessage(userMsg);el.composer.dispatchEvent(new CustomEvent('user:send',{detail:{message:userMsg}}));if(shouldGenTitle)(async()=>{const title=await services.generateTitleWithAI(api.state.messages)||ui.partsToText(api.state.messages.find(m=>m.role==='user')?.content)||'Untitled';await api.THREAD.setTitle(th.id,title)})();if(!SUNE_PROXY.model)return api.state.attachments=[],ui.updateAttachBadge();api.state.busy=true;ui.setBtnStop();const a=SUNE_PROXY.active,suneMeta={sune_name:a.name,model:SUNE_PROXY.model,avatar:a.avatar||''},streamId=utils.sid(),suneBubble=ui.addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);api.state.messages.push(assistantMsg);api.THREAD.persist(false);api.state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done)=>{buf+=delta;api.state.stream.text=buf;ui.renderMarkdown(suneBubble,buf,{enhance:false});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;ui.setBtnSend();api.state.busy=false;ui.enhanceCodeBlocks(suneBubble,true);api.THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));api.state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)api.THREAD.persist(false)};await services.askOpenRouterStreaming(onDelta,streamId);api.state.attachments=[];ui.updateAttachBadge()});
$(el.suneBtnTop).on('click',ui.openSettings);
$(el.cancelSettings).on('click',ui.closeSettings);
$(el.suneModal).on('click',e=>{if(e.target===el.suneModal||e.target.classList.contains('bg-black/30'))ui.closeSettings()});
$(el.tabModel).on('click',()=>ui.showTab('Model'));
$(el.tabPrompt).on('click',()=>ui.showTab('Prompt'));
$(el.tabScript).on('click',()=>ui.showTab('Script'));
$(el.settingsForm).on('submit',async e=>{e.preventDefault();SUNE_PROXY.url=(el.suneURL.value||'').trim();SUNE_PROXY.model=(el.set_model.value||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a'].forEach(k=>SUNE_PROXY[k]=el[`set_${k}`].value.trim());SUNE_PROXY.verbosity=(el.set_verbosity.value||'');SUNE_PROXY.reasoning_effort=(el.set_reasoning_effort.value||'default');SUNE_PROXY.system_prompt=el.set_system_prompt.value.trim();SUNE_PROXY.hide_composer=el.set_hide_composer.checked;SUNE_PROXY.json_output=el.set_json_output.checked;SUNE_PROXY.include_thoughts=el.set_include_thoughts.checked;SUNE_PROXY.ignore_master_prompt=el.set_ignore_master_prompt.checked;SUNE_PROXY.json_schema=el.jsonSchemaEditor.textContent;if(window.openedHTML){SUNE_PROXY.html=el.htmlEditor.textContent;SUNE_PROXY.extension_html=el.extensionHtmlEditor.textContent}ui.closeSettings();await ui.reflectActiveSune()});
$(el.deleteSuneBtn).on('click',async()=>{const activeId=SUNE_PROXY.id,name=SUNE_PROXY.name||'this sune';if(!confirm(`Delete "${name}"?`))return;SUNE_PROXY.delete(activeId);ui.renderSidebar();await ui.reflectActiveSune();api.state.currentThreadId=null;ui.clearChat();ui.closeSettings()});
$(el.newSuneBtn).on('click',async()=>{const name=prompt('Name your sune:');if(!name)return;const sune=SUNE_PROXY.create({name:name.trim()});SUNE_PROXY.setActive(sune.id);ui.renderSidebar();await ui.reflectActiveSune();api.state.currentThreadId=null;ui.clearChat();el.sidebarLeft.classList.add('-translate-x-full');el.sidebarOverlayLeft.classList.add('hidden')});
$(el.sunesExportOption).on('click',()=>{utils.dl(`sunes-${utils.ts()}.sune`,{version:1,sunes:SUNE_PROXY.list,activeId:SUNE_PROXY.id});el.userMenu.classList.add('hidden')});
$(el.sunesImportOption).on('click',()=>{window.importMode='sunes';el.importInput.value='';el.importInput.click()});
$(el.threadsExportOption).on('click',()=>{utils.dl(`threads-${utils.ts()}.json`,{version:1,threads:api.THREAD.list});el.userMenu.classList.add('hidden')});
$(el.threadsImportOption).on('click',()=>{window.importMode='threads';el.importInput.value='';el.importInput.click()});
$(el.importInput).on('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const text=await file.text();const data=JSON.parse(text);if(window.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=>api.makeSune(a||{}));const map={};incoming.forEach(s=>{if(!s.id)s.id=utils.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(api.sunes.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex){api.sunes.push(s);added++}else if(+s.updatedAt>+ex.updatedAt){Object.assign(ex,s);updated++}});SUNE_PROXY.save();if(data.activeId&&api.sunes.some(x=>x.id===data.activeId))SUNE_PROXY.setActive(data.activeId);ui.renderSidebar();await ui.reflectActiveSune();api.state.currentThreadId=null;ui.clearChat();alert(`${added} new, ${updated} updated.`)}else if(window.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||utils.gid(),title:utils.titleFrom(t.title||utils.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(api.THREAD.list.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)api.THREAD.list.push(th);else Object.assign(ex,th);kept++}await api.THREAD.save();await ui.renderThreads();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{window.importMode=null}});
$(el.attachBtn).on('click',()=>{if(api.state.busy)return;if(api.state.attachments.length){api.state.attachments=[];ui.updateAttachBadge();el.fileInput.value=''};el.fileInput.click()});
$(el.fileInput).on('change',async()=>{const files=[...(el.fileInput.files||[])];if(!files.length)return;for(const f of files){const at=await ui.toAttach(f).catch(()=>null);if(at)api.state.attachments.push(at)}ui.updateAttachBadge()});
$(el.threadList).on('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!==api.state.currentThreadId&&api.state.busy){api.state.controller?.disconnect?.();ui.setBtnSend();api.state.busy=false;api.state.controller=null}const th=api.THREAD.get(id);if(!th)return;if(id===api.state.currentThreadId){el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');ui.hideThreadPopover();return}api.state.currentThreadId=id;ui.clearChat();api.state.messages=Array.isArray(th.messages)?[...th.messages]:[];for(const m of api.state.messages){const b=ui.msgRow(m);b.dataset.mid=m.id||'';ui.renderMarkdown(b,ui.partsToText(m.content))}await ui.renderSuneHTML();syncWhileBusy();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');ui.hideThreadPopover();return}if(menuBtn){e.stopPropagation();ui.showThreadPopover(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}});
$(el.threadList).on('scroll',()=>{const THREAD_PAGE_SIZE=50;if(window.isAddingThreads||el.threadList.scrollTop+el.threadList.clientHeight<el.threadList.scrollHeight-200)return;const c=el.threadList.children.length;if(c>=window.sortedThreads.length)return;window.isAddingThreads=true;const b=window.sortedThreads.slice(c,c+THREAD_PAGE_SIZE);if(b.length){el.threadList.insertAdjacentHTML('beforeend',b.map(ui.threadRow).join(''));ui.icons()}window.isAddingThreads=false});
$(el.threadPopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!window.menuThreadId)return;const th=api.THREAD.get(window.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=utils.titleFrom(nv);th.updatedAt=Date.now()}}else if(act==='delete'){if(confirm('Delete this chat?')){api.THREAD.list=api.THREAD.list.filter(x=>x.id!==th.id);if(api.state.currentThreadId===th.id){api.state.currentThreadId=null;ui.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(ui.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+')')}ui.hideThreadPopover();await api.THREAD.save();ui.renderThreads()});
$(el.suneList).on('click',async e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();ui.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(api.state.busy){api.state.controller?.disconnect?.();ui.setBtnSend();api.state.busy=false;api.state.controller=null};SUNE_PROXY.setActive(id);ui.renderSidebar();await ui.reflectActiveSune();api.state.currentThreadId=null;ui.clearChat();el.sidebarLeft.classList.add('-translate-x-full');el.sidebarOverlayLeft.classList.add('hidden')}});
$(el.sunePopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!window.menuSuneId)return;const s=SUNE_PROXY.get(window.menuSuneId);if(!s)return;const updateAndRender=async()=>{s.updatedAt=Date.now();SUNE_PROXY.save();ui.renderSidebar();await ui.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=async()=>{const f=i.files?.[0];if(!f)return;try{s.avatar=await utils.imgToWebp(f);await updateAndRender()}catch{}};i.click()}else if(act==='export')utils.dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${utils.ts()}.sune`,[s]);ui.hideSunePopover()});
el.htmlTab_index.onclick=()=>ui.showHtmlTab('index');el.htmlTab_extension.onclick=()=>ui.showHtmlTab('extension');
el.accountTabGeneral.onclick=()=>ui.showAccountTab('General');el.accountTabAPI.onclick=()=>ui.showAccountTab('API');el.accountTabUser.onclick=()=>ui.showAccountTab('User');
$(el.accountSettingsOption).on('click',()=>{el.userMenu.classList.add('hidden');ui.openAccountSettings()});
$(el.closeAccountSettings).on('click',ui.closeAccountSettings);
$(el.cancelAccountSettings).on('click',ui.closeAccountSettings);
$(el.accountSettingsModal).on('click',e=>{if(e.target===el.accountSettingsModal||e.target.classList.contains('bg-black/30'))ui.closeAccountSettings()});
$(el.accountSettingsForm).on('submit',e=>{e.preventDefault();api.USER.provider=el.set_provider.value||'openrouter';api.USER.apiKeyOpenRouter=String(el.set_api_key_or.value||'').trim();api.USER.apiKeyOpenAI=String(el.set_api_key_oai.value||'').trim();api.USER.apiKeyGoogle=String(el.set_api_key_g.value||'').trim();api.USER.apiKeyClaude=String(el.set_api_key_claude.value||'').trim();api.USER.apiKeyCloudflare=String(el.set_api_key_cf.value||'').trim();api.USER.masterPrompt=String(el.set_master_prompt.value||'').trim();api.USER.titleModel=String(el.set_title_model.value||'').trim();api.USER.githubToken=String(el.set_gh_token.value||'').trim();api.USER.name=String(el.set_user_name.value||'').trim();ui.closeAccountSettings()});
el.gcpSAUploadBtn.onclick=()=>el.gcpSAInput.click();el.gcpSAInput.onchange=async e=>{const f=e.target.files?.[0];if(!f)return;try{const t=await f.text(),d=JSON.parse(t);if(!d.project_id)throw new Error('Invalid');api.USER.gcpSA=d;el.gcpSAUploadBtn.textContent=`Uploaded: ${d.project_id}`;alert('GCP SA loaded.')}catch{alert('Failed to load GCP SA.')}};
$(el.accountPanelAPI).on('click',e=>{const b=e.target.closest('[data-reveal-for]');if(!b)return;const i=document.getElementById(b.dataset.revealFor);if(!i)return;const p=i.type==='password';i.type=p?'text':'password';b.querySelector('i').setAttribute('data-lucide',p?'eye-off':'eye');lucide.createIcons()});
el.setUserAvatarBtn.onclick=()=>el.userAvatarInput.click();el.userAvatarInput.onchange=async e=>{const f=e.target.files?.[0];if(!f)return;try{const dataUrl=await utils.imgToWebp(f);api.USER.avatar=dataUrl;el.userAvatarPreview.src=dataUrl;el.userAvatarPreview.classList.remove('bg-gray-200')}catch{alert('Failed to process image.')}};
el.exportAccountSettings.onclick=()=>utils.dl(`sune-account-${utils.ts()}.json`,{v:1,provider:api.USER.provider,apiKeyOpenRouter:api.USER.apiKeyOpenRouter,apiKeyOpenAI:api.USER.apiKeyOpenAI,apiKeyGoogle:api.USER.apiKeyGoogle,apiKeyClaude:api.USER.apiKeyClaude,apiKeyCloudflare:api.USER.apiKeyCloudflare,masterPrompt:api.USER.masterPrompt,titleModel:api.USER.titleModel,githubToken:api.USER.githubToken,gcpSA:api.USER.gcpSA,userName:api.USER.name,userAvatar:api.USER.avatar});
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');const m={provider:'provider',apiKeyOpenRouter:'apiKeyOR',apiKeyOpenAI:'apiKeyOAI',apiKeyGoogle:'apiKeyG',apiKeyClaude:'apiKeyC',apiKeyCloudflare:'apiKeyCF',masterPrompt:'masterPrompt',titleModel:'titleModel',githubToken:'ghToken',name:'userName',avatar:'userAvatar',gcpSA:'gcpSA'};Object.entries(m).forEach(([p,k])=>{const v=d[p]??d[k];if(typeof v==='string'||(p==='gcpSA'&&typeof v==='object'&&v))api.USER[p]=v});ui.openAccountSettings();alert('Imported.')}catch{alert('Import failed')}};
$(document).on('visibilitychange',ui.onForeground);
$(window).on('resize',()=>{ui.hideThreadPopover();ui.hideSunePopover()});
$(el.copySystemPrompt).on('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}});
$(el.pasteSystemPrompt).on('click',async()=>{try{el.set_system_prompt.value=await navigator.clipboard.readText()}catch{}});
$(el.copyHTML).on('click',async()=>{try{await navigator.clipboard.writeText(ui.getActiveHtmlParts()[0].textContent||'')}catch{}});
$(el.pasteHTML).on('click',async()=>{try{const t=await navigator.clipboard.readText();const[editor,jar]=ui.getActiveHtmlParts();if(jar&&jar.updateCode)jar.updateCode(t);else if(editor)editor.textContent=t}catch{}});
}
async function init(){
ui.initDOM();
Object.assign(window,{SUNE:SUNE_PROXY,USER:api.USER,THREAD:api.THREAD,state:api.state,sunes:api.sunes,...api,...utils,...services,...ui,syncWhileBusy});
await SUNE_PROXY.fetchDotSune('sune-org/store@main/marketplace.sune');
await SUNE_PROXY.fetchDotSune('sune-org/store@main/forum.sune');
await api.THREAD.load();
await ui.renderThreads();
ui.renderSidebar();
await ui.reflectActiveSune();
ui.clearChat();
ui.icons();
ui.kbBind();
ui.kbUpdate();
el.htmlTab_index.textContent='index.html';
el.htmlTab_extension.textContent='extension.html';
}
document.addEventListener('DOMContentLoaded',init);