mirror of
https://github.com/multipleof4/sune.git
synced 2026-01-14 08:38:00 +00:00
59 lines
17 KiB
JavaScript
59 lines
17 KiB
JavaScript
import{el}from'./dom.js';import{icons,dl,ts,imgToWebp}from'./utils.js';import{state}from'./state.js';import{SUNE,THREAD,USER,makeSune,ensureThreadOnFirstUser,titleFrom}from'./models.js';import{renderSuneHTML,reflectActiveSune,renderSidebar,clearChat,setBtnSend,renderMarkdown,partsToText,updateAttachBadge,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,renderThreads,menuThreadId,menuSuneId,setMenuThreadId,setMenuSuneId,ensureJars,openSettings,closeSettings,showTab,showHtmlTab,openedHTML,openAccountSettings,closeAccountSettings,showAccountTab,kbBind,kbUpdate,getBubbleById}from'./ui.js';
|
|
import{cacheStore}from'./storage.js';
|
|
(()=>{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()))}})()
|
|
async function init(){await SUNE.fetchDotSune('sune-org/store@main/marketplace.sune');await SUNE.fetchDotSune('sune-org/store@main/forum.sune');await THREAD.load();await renderThreads();renderSidebar();await reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
|
|
let syncLoopRunning=!1;
|
|
async function syncActiveThread(){const id=THREAD.getLastAssistantMessageId();if(!id)return!1;if(await cacheStore.getItem(id)==='done'){if(state.busy){setBtnSend();state.busy=!1;state.controller=null}return!1}if(!state.busy){state.busy=!0;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!1;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:!1});enhanceCodeBlocks(bubble,!0);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()});THREAD.persist();setBtnSend();state.busy=!1;cacheStore.setItem(id,'done');state.controller=null;el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:state.messages.find(m=>m.id===id)}}))};if(!j||j.rid!==id){if(j&&j.error){const t=prevText+'\n\n'+j.error;finalise(t,[{type:'text',text:t}])}return!1}const text=j.text||'',isDone=j.error||j.done||j.phase==='done';if(text)renderMarkdown(bubble,text,{enhance:!1});if(isDone){const finalText=text||prevText;finalise(finalText,[{type:'text',text:finalText}]);return!1}await cacheStore.setItem(id,'busy');return!0}
|
|
async function syncWhileBusy(){if(syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=!0;try{while(await syncActiveThread())await new Promise(r=>setTimeout(r,1500))}finally{syncLoopRunning=!1}}
|
|
const onForeground=()=>{if(document.visibilityState!=='visible')return;state.controller?.disconnect?.();if(state.busy)syncWhileBusy()}
|
|
$(window).on('resize',()=>{hideThreadPopover();hideSunePopover()})
|
|
$(document).on('visibilitychange',onForeground)
|
|
$(el.sidebarBtnLeft).on('click',()=>{el.sidebarLeft.classList.remove('-translate-x-full');el.sidebarOverlayLeft.classList.remove('hidden')})
|
|
$(el.sidebarOverlayLeft).on('click',()=>{el.sidebarLeft.classList.add('-translate-x-full');el.sidebarOverlayLeft.classList.add('hidden');el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();hideSunePopover()})
|
|
$(el.sidebarBtnRight).on('click',()=>{renderThreads();el.sidebarRight.classList.remove('translate-x-full');el.sidebarOverlayRight.classList.remove('hidden')})
|
|
$(el.sidebarOverlayRight).on('click',()=>{el.sidebarOverlayRight.classList.add('hidden');el.sidebarRight.classList.add('translate-x-full')})
|
|
$(el.closeThreads).on('click',()=>{el.sidebarOverlayRight.classList.add('hidden');el.sidebarRight.classList.add('translate-x-full')})
|
|
$(el.suneBtnTop).on('click',openSettings)
|
|
$(el.cancelSettings).on('click',closeSettings)
|
|
$(el.suneModal).on('click',e=>{if(e.target===el.suneModal||e.target.classList.contains('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.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;SUNE.json_output=el.set_json_output.checked;SUNE.include_thoughts=el.set_include_thoughts.checked;SUNE.ignore_master_prompt=el.set_ignore_master_prompt.checked;SUNE.json_schema=el.jsonSchemaEditor.textContent;if(openedHTML){SUNE.html=el.htmlEditor.textContent;SUNE.extension_html=el.extensionHtmlEditor.textContent}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.classList.add('-translate-x-full');el.sidebarOverlayLeft.classList.add('hidden')})
|
|
$(el.userMenuBtn).on('click',e=>{e.stopPropagation();el.userMenu.classList.toggle('hidden')})
|
|
let importMode=null
|
|
$(el.sunesExportOption).on('click',()=>{dl(`sunes-${ts()}.sune`,{version:1,sunes:SUNE.list,activeId:SUNE.id});el.userMenu.classList.add('hidden')})
|
|
$(el.sunesImportOption).on('click',()=>{importMode='sunes';el.importInput.value='';el.importInput.click()})
|
|
$(el.threadsExportOption).on('click',()=>{dl(`threads-${ts()}.json`,{version:1,threads:THREAD.list});el.userMenu.classList.add('hidden')})
|
|
$(el.threadsImportOption).on('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()})
|
|
$(el.importInput).on('change',async()=>{const file=el.importInput.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:[]);if(!list.length)throw new Error('No sunes');const incoming=list.map(a=>makeSune(a||{})),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(SUNE.list.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex){SUNE.list.push(s);added++}else if(+s.updatedAt>+ex.updatedAt){Object.assign(ex,s);updated++}});SUNE.save();if(data.activeId&&SUNE.list.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(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)THREAD.list.push(th);else Object.assign(ex,th);kept++}await THREAD.save();await renderThreads();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
|
|
$(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!==state.currentThreadId&&state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=!1;state.controller=null}const th=THREAD.get(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'))}})
|
|
$(el.threadList).on('scroll',()=>{if(isAddingThreads||el.threadList.scrollTop+el.threadList.clientHeight<el.threadList.scrollHeight-200)return;const c=el.threadList.children.length;if(c>=sortedThreads.length)return;isAddingThreads=!0;const b=sortedThreads.slice(c,c+THREAD_PAGE_SIZE);if(b.length){el.threadList.insertAdjacentHTML('beforeend',b.map(threadRow).join(''));icons()}isAddingThreads=!1});
|
|
$(el.threadPopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=THREAD.get(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)}else if(act==='delete'){if(confirm('Delete this chat?')){THREAD.list=THREAD.list.filter(x=>x.id!==th.id);if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){let totalChars=0;for(const m of(Array.isArray(th.messages)?th.messages:[])){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m.content||'')||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4)),k=tokens>=1e3?Math.round(tokens/1e3)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}hideThreadPopover();await THREAD.save();renderThreads()})
|
|
$(el.suneList).on('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'));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=!1;state.controller=null};SUNE.setActive(id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;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||!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=async()=>{const f=i.files?.[0];if(!f)return;try{s.avatar=await imgToWebp(f);await updateAndRender()}catch{}};i.click()}else if(act==='export')dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${ts()}.sune`,[s]);hideSunePopover()})
|
|
$(el.attachBtn).on('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];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 toAttach(f).catch(()=>null);if(at)state.attachments.push(at)}updateAttachBadge()})
|
|
$(el.composer).on('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return SUNE.infer();await ensureThreadOnFirstUser(text||'(attachments)');const th=THREAD.active,shouldGenTitle=th&&!th.title;el.input.value='';const parts=[];if(text)parts.push({type:'text',text});parts.push(...state.attachments);const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};addMessage(userMsg);el.composer.dispatchEvent(new CustomEvent('user:send',{detail:{message:userMsg}}));if(shouldGenTitle)(async()=>{const title=await generateTitleWithAI(state.messages)||partsToText(state.messages.find(m=>m.role==='user')?.content)||'Untitled';await THREAD.setTitle(th.id,title)})();if(!SUNE.model)return state.attachments=[],updateAttachBadge();SUNE.infer();state.attachments=[];updateAttachBadge()})
|
|
el.htmlTab_index.textContent='index.html';el.htmlTab_extension.textContent='extension.html';
|
|
el.htmlTab_index.onclick=()=>showHtmlTab('index');el.htmlTab_extension.onclick=()=>showHtmlTab('extension');
|
|
$(el.copySystemPrompt).on('click',async()=>await navigator.clipboard.writeText(el.set_system_prompt.value||'').catch(()=>{}));
|
|
$(el.pasteSystemPrompt).on('click',async()=>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).on('click',async()=>await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent||'').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.textContent=t}catch{}});
|
|
$(el.accountSettingsOption).on('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()})
|
|
$(el.closeAccountSettings).on('click',closeAccountSettings)
|
|
$(el.cancelAccountSettings).on('click',closeAccountSettings)
|
|
$(el.accountSettingsModal).on('click',e=>{if(e.target===el.accountSettingsModal||e.target.classList.contains('bg-black/30'))closeAccountSettings()})
|
|
$(el.accountSettingsForm).on('submit',e=>{e.preventDefault();USER.provider=el.set_provider.value||'openrouter';USER.apiKeyOpenRouter=String(el.set_api_key_or.value||'').trim();USER.apiKeyOpenAI=String(el.set_api_key_oai.value||'').trim();USER.apiKeyGoogle=String(el.set_api_key_g.value||'').trim();USER.apiKeyClaude=String(el.set_api_key_claude.value||'').trim();USER.apiKeyCloudflare=String(el.set_api_key_cf.value||'').trim();USER.masterPrompt=String(el.set_master_prompt.value||'').trim();USER.titleModel=String(el.set_title_model.value||'').trim();USER.githubToken=String(el.set_gh_token.value||'').trim();USER.name=String(el.set_user_name.value||'').trim();closeAccountSettings()})
|
|
el.gcpSAUploadBtn.onclick=()=>el.gcpSAInput.click();el.gcpSAInput.onchange=async e=>{const f=e.target.files?.[0];if(!f)return;try{const d=JSON.parse(await f.text());if(!d.project_id)throw new Error('Invalid');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');icons()});
|
|
el.accountTabGeneral.onclick=()=>showAccountTab('General');el.accountTabAPI.onclick=()=>showAccountTab('API');el.accountTabUser.onclick=()=>showAccountTab('User')
|
|
el.setUserAvatarBtn.onclick=()=>el.userAvatarInput.click();el.userAvatarInput.onchange=async e=>{const f=e.target.files?.[0];if(!f)return;try{const dataUrl=await imgToWebp(f);USER.avatar=dataUrl;el.userAvatarPreview.src=dataUrl;el.userAvatarPreview.classList.remove('bg-gray-200')}catch{alert('Failed to process image.')}}
|
|
el.exportAccountSettings.onclick=()=>dl(`sune-account-${ts()}.json`,{v:1,provider:USER.provider,apiKeyOpenRouter:USER.apiKeyOpenRouter,apiKeyOpenAI:USER.apiKeyOpenAI,apiKeyGoogle:USER.apiKeyGoogle,apiKeyClaude:USER.apiKeyClaude,apiKeyCloudflare:USER.apiKeyCloudflare,masterPrompt:USER.masterPrompt,titleModel:USER.titleModel,githubToken:USER.githubToken,gcpSA:USER.gcpSA,userName:USER.name,userAvatar:USER.avatar});
|
|
el.importAccountSettings.onclick=()=>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))USER[p]=v});openAccountSettings();alert('Imported.')}catch{alert('Import failed')}};
|
|
init()
|