mirror of
https://github.com/multipleof4/sune.git
synced 2026-01-13 16:17:55 +00:00
Refactor: Consolidate data models and logic
This commit is contained in:
14
src/models.js
Normal file
14
src/models.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import{state}from'./state.js';import{gid,sid,esc,num,int,b64,asDataURL,imgToWebp,clamp}from'./utils.js';import{addMessage,addSuneBubbleStreaming,setBtnSend,setBtnStop,renderMarkdown,enhanceCodeBlocks,renderSidebar,reflectActiveSune,clearChat,updateAttachBadge,partsToText,renderThreads}from'./ui.js';import{askOpenRouterStreaming,generateTitleWithAI}from'./api.js';import{el}from'./dom.js';
|
||||
const DEFAULT_MODEL='google/gemini-2.5-pro',DEFAULT_API_KEY=''
|
||||
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></sune>",hide_composer:!1,include_thoughts:!1,json_output:!1,ignore_master_prompt:!1,json_schema:''}
|
||||
export 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:{...defaultSettings,...p.settings||{}},storage:p.storage||{}})
|
||||
let sunes=(su.load()||[]).map(makeSune)
|
||||
const activeMeta=()=>({sune_name:SUNE.name,model:SUNE.model,avatar:SUNE.avatar})
|
||||
const toAttach=async f=>{if(!f)return null;if(f instanceof File){const n=f.name||'file',m=(f.type||'application/octet-stream').toLowerCase();if(/^image\//.test(m)||/\.(png|jpe?g|webp|gif)$/i.test(n)){const d=m==='image/webp'||/\.webp$/i.test(n)?await asDataURL(f):await imgToWebp(f,2048,94);return{type:'image_url',image_url:{url:d}}}if(m==='application/pdf'||/\.pdf$/i.test(n)){const d=await asDataURL(f);return{type:'file',file:{filename:n.endsWith('.pdf')?n:n+'.pdf',file_data:b64(d)}}}if(/^audio\//.test(m)||/\.(wav|mp3)$/i.test(n)){const d=await asDataURL(f),fmt=/mp3/.test(m)||/\.mp3$/i.test(n)?'mp3':'wav';return{type:'input_audio',input_audio:{data:b64(d),format:fmt}}}const d=await asDataURL(f);return{type:'file',file:{filename:n,file_data:b64(d)}}}if(f&&f.name==null&&f.data){const n=f.name||'file',m=(f.mime||'application/octet-stream').toLowerCase();if(/^image\//.test(m))return{type:'image_url',image_url:{url:`data:${m};base64,${f.data}`}};if(m==='application/pdf')return{type:'file',file:{filename:n,file_data:f.data}};if(/^audio\//.test(m)){const fmt=/mp3/.test(m)?'mp3':'wav';return{type:'input_audio',input_audio:{data:f.data,format:fmt}}}return{type:'file',file:{filename:n,file_data:f.data}}}return null}
|
||||
export 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(makeSune));sunes.length>l&&t.save()}catch{}};if(p==='attach')return async fs=>{const a=[];for(const f of fs||[])a.push(await toAttach(f));const c=a.filter(Boolean);if(!c.length)return;await ensureThreadOnFirstUser('(attachments)');addMessage({role:'assistant',content:c,...activeMeta()});await THREAD.persist()};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 THREAD.persist()};if(p==='lastReply')return [...state.messages].reverse().find(m=>m.role==='assistant');if(p==='infer')return async()=>{if(state.busy||!SUNE.model||state.abortRequested){state.abortRequested=!1;return};await ensureThreadOnFirstUser('Sune Inference');const th=THREAD.active;if(th&&!th.title)(async()=>THREAD.setTitle(th.id,await generateTitleWithAI(state.messages)||'Sune Inference'))();state.busy=!0;setBtnStop();const a=SUNE.active,meta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},id=sid(),bubble=addSuneBubbleStreaming(meta,id);bubble.dataset.mid=id;const msg={id,role:'assistant',content:[{type:'text',text:''}],...meta};state.messages.push(msg);THREAD.persist(!1);state.stream={rid:id,bubble,meta,text:'',done:!1};let buf='',completed=!1;const onDelta=(d,done)=>{buf+=d;state.stream.text=buf;renderMarkdown(bubble,buf,{enhance:!1});msg.content[0].text=buf;if(done&&!completed){completed=!0;setBtnSend();state.busy=!1;enhanceCodeBlocks(bubble,!0);THREAD.persist(!0);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:msg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:!1}}else if(!done)THREAD.persist(!1)};await askOpenRouterStreaming(onDelta,id)};if(p==='getByName')return n=>sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(p==='handoff')return async n=>{await new Promise(r=>setTimeout(r,4000));const s=sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(!s)return;SUNE.setActive(s.id);renderSidebar();await reflectActiveSune();await SUNE.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!1;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return!1;const isTop=/^(name|avatar|url|pinned|storage)$/.test(p),target=isTop?sunes[i]:sunes[i].settings;let value=v;if(!isTop&&p==='system_prompt')value=v||'';if(target[p]!==value){target[p]=value;sunes[i].updatedAt=Date.now();su.save(sunes)}return!0}})
|
||||
if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)}
|
||||
const TKEY='threads_v1';export const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled'
|
||||
export const THREAD=window.THREAD={list:[],load:async function(){this.list=await localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[])||[]},save:async function(){await localforage.setItem(TKEY,this.list)},get:function(id){return this.list.find(t=>t.id===id)},get active(){return this.get(state.currentThreadId)},persist:async function(full=true){if(!state.currentThreadId)return;const th=this.active;if(!th)return;th.messages=[...state.messages];if(full)th.updatedAt=Date.now();await this.save();if(full)await renderThreads()},setTitle:async function(id,title){const th=this.get(id);if(!th||!title)return;th.title=titleFrom(title);th.updatedAt=Date.now();await this.save();await renderThreads()},getLastAssistantMessageId:()=>{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&&!/^\s*You\b/.test(h.textContent||''))return b.dataset.mid||null}return null}}
|
||||
export async function ensureThreadOnFirstUser(text){let needNew=!state.currentThreadId;if(state.messages.length===0)state.currentThreadId=null;if(state.currentThreadId&&!THREAD.get(state.currentThreadId))needNew=!0;if(!needNew)return;const id=gid(),now=Date.now(),th={id,title:'',pinned:!1,updatedAt:now,messages:[]};state.currentThreadId=id;THREAD.list.unshift(th);await THREAD.save();await renderThreads()}
|
||||
export const USER=window.USER={async log(s){const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'user',content:[{type:'text',text:t}]});await THREAD.persist()},async logMany(msgs){if(!Array.isArray(msgs)||!msgs.length)return;const clean=msgs.map(s=>String(s??'').trim()).filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser(clean[0]);const newMsgs=clean.map(t=>({id:gid(),role:'user',content:[{type:'text',text:t}]}));state.messages.push(...newMsgs);for(const m of newMsgs)addMessage(m,!1);await THREAD.persist()},get PAT(){return this.githubToken},get name(){return localStorage.getItem('user_name')||'Anon'},set name(v){localStorage.setItem('user_name',v||'')},get avatar(){return localStorage.getItem('user_avatar')||''},set avatar(v){localStorage.setItem('user_avatar',v||'')},get provider(){return localStorage.getItem('provider')||'openrouter'},set provider(v){localStorage.setItem('provider',['openai','google','claude','cloudflare'].includes(v)?v:'openrouter')},get apiKeyOpenRouter(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKeyOpenRouter(v){localStorage.setItem('openrouter_api_key',v||'')},get apiKeyOpenAI(){return localStorage.getItem('openai_api_key')||''},set apiKeyOpenAI(v){localStorage.setItem('openai_api_key',v||'')},get apiKeyGoogle(){return localStorage.getItem('google_api_key')||''},set apiKeyGoogle(v){localStorage.setItem('google_api_key',v||'')},get apiKeyClaude(){return localStorage.getItem('claude_api_key')||''},set apiKeyClaude(v){localStorage.setItem('claude_api_key',v||'')},get apiKeyCloudflare(){return localStorage.getItem('cloudflare_api_key')||''},set apiKeyCloudflare(v){localStorage.setItem('cloudflare_api_key',v||'')},get apiKey(){const p=this.provider;return p==='openai'?this.apiKeyOpenAI:p==='google'?this.apiKeyGoogle:p==='claude'?this.apiKeyClaude:p==='cloudflare'?this.apiKeyCloudflare:this.apiKeyOpenRouter},set apiKey(v){const p=this.provider;if(p==='openai')this.apiKeyOpenAI=v;else if(p==='google')this.apiKeyGoogle=v;else if(p==='claude')this.apiKeyClaude=v;else if(p==='cloudflare')this.apiKeyCloudflare=v;else this.apiKeyOpenRouter=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 githubToken(){return localStorage.getItem('gh_token')||''},set githubToken(v){localStorage.setItem('gh_token',v||'')},get gcpSA(){try{return JSON.parse(localStorage.getItem('gcp_sa_json')||'null')}catch{return null}},set gcpSA(v){localStorage.setItem('gcp_sa_json',v?JSON.stringify(v):'')}}
|
||||
Reference in New Issue
Block a user