diff --git a/src/main.js b/src/main.js index 487683f..d231563 100644 --- a/src/main.js +++ b/src/main.js @@ -105,14 +105,14 @@ function kbUpdate(){const vv=window.visualViewport;const overlap=vv?Math.max(0,( function kbBind(){if(window.visualViewport){['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,()=>kbUpdate(),{passive:true}))}$(window).on('resize orientationchange',()=>setTimeout(kbUpdate,50));$(el.input).on('focus click',()=>{setTimeout(()=>{kbUpdate();el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)})} function activeMeta(){return {sune_name:SUNE.name,model:SUNE.model,avatar:SUNE.avatar}} const USER=window.USER={log:async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'user',content:[{type:'text',text:t}]});await THREAD.persist()},logMany:async 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);const frag=document.createDocumentFragment();const newEls=newMsgs.map(m=>{const $row=_createMessageRow(m),bubble=$row.find('.msg-bubble')[0];bubble.dataset.mid=m.id;return{rowEl:$row[0],bubbleEl:bubble,message:m}});newEls.forEach(item=>frag.appendChild(item.rowEl));el.messages.appendChild(frag);queueMicrotask(()=>{newEls.forEach(item=>{renderMarkdown(item.bubbleEl,partsToText(item.message))});el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'});icons()});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'].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.'},set masterPrompt(v){localStorage.setItem('master_prompt',v||'')},get donor(){return localStorage.getItem('user_donor')!=='false'},set donor(v){localStorage.setItem('user_donor',String(!!v))},get titleModel(){return localStorage.getItem('title_model')??'or:amazon/nova-micro-v1'},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):'')}} -async function init(){el.threadRepoInput.value=localStorage.getItem('thread_repo_url')||'';await THREAD.load();await renderThreads();renderSidebar();await reflectActiveSune();clearChat();icons();kbBind();kbUpdate()} +async function init(){const u=localStorage.getItem('thread_repo_url')||'';el.threadRepoInput.value=u;el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);await THREAD.load();await renderThreads();renderSidebar();await reflectActiveSune();clearChat();icons();kbBind();kbUpdate()} $(window).on('resize',()=>{hideThreadPopover();hideSunePopover()}) const htmlTabs={index:['htmlTab_index','htmlEditor'],extension:['htmlTab_extension','extensionHtmlEditor']};function showHtmlTab(key){Object.entries(htmlTabs).forEach(([k,[tb,pn]])=>{const a=k===key;el[tb].classList.toggle('border-black',a);el[tb].classList.toggle('border-transparent',!a);el[tb].classList.toggle('hover:border-gray-300',!a);el[pn].classList.toggle('hidden',!a)})} el.htmlTab_index.textContent='index.html';el.htmlTab_extension.textContent='extension.html'; el.htmlTab_index.onclick=()=>showHtmlTab('index');el.htmlTab_extension.onclick=()=>showHtmlTab('extension'); const ghApi=async(path,method='GET',body=null)=>{const t=USER.githubToken;if(!t)throw new Error('No GH token');const r=await fetch(`https://api.github.com/repos/${path}`,{method,headers:{'Authorization':`token ${t}`,'Accept':'application/vnd.github.v3+json','Content-Type':'application/json'},body:body?JSON.stringify(body):null});if(!r.ok&&r.status!==404)throw new Error(`GH API ${r.status}`);return r.status===404?null:r.json()}; const parseGhUrl=u=>{const p=u.substring(5).split('/'),owner=p[0],repo=p[1],branch=repo?.includes('@')?repo.split('@')[1]:'main',cleanRepo=repo?.split('@')[0],path=p.slice(2).join('/');return{owner,repo:cleanRepo,branch,path,full:`${owner}/${cleanRepo}/contents/${path?path+'/':''}index.json?ref=${branch}`,dir:`${owner}/${cleanRepo}/contents/${path?path+'/':' '}`.trim()}}; -$(el.threadRepoInput).on('change',async()=>{const u=el.threadRepoInput.value.trim();localStorage.setItem('thread_repo_url',u);el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);await THREAD.load();await renderThreads()}); +$(el.threadRepoInput).on('change',async()=>{const u=el.threadRepoInput.value.trim();localStorage.setItem('thread_repo_url',u);el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);await THREAD.load();await renderThreads()}); $(el.threadBackBtn).on('click',()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const p=u.split('/');if(p.length>3){p.pop();el.threadRepoInput.value=p.join('/');el.threadRepoInput.dispatchEvent(new Event('change'))}}); $(el.threadFolderBtn).on('click',async()=>{const n=prompt('Folder name:');if(!n)return;THREAD.list.unshift({id:n.trim(),title:n.trim(),type:'folder',updatedAt:Date.now()});await THREAD.save();await renderThreads()}); $(el.threadSyncBtn).on('click',async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const mode=confirm('Sync Threads:\nOK = Upload (Push)\nCancel = Download (Pull)');const info=parseGhUrl(u);try{if(mode){const idxFile=await ghApi(info.full),sha=idxFile?.sha;for(const t of THREAD.list){if(t.type==='thread'&&(t.status==='modified'||t.status==='new')){const msgs=await localforage.getItem('rem_t_'+t.id),fPath=`${info.dir}${t.id}.json`,ex=await ghApi(fPath+'?ref='+info.branch);await ghApi(fPath,'PUT',{message:`Sync thread ${t.id}`,content:utob(JSON.stringify(msgs)),branch:info.branch,sha:ex?.sha});t.status='synced'}}await ghApi(info.full,'PUT',{message:'Update index.json',content:utob(JSON.stringify(THREAD.list)),branch:info.branch,sha});alert('Pushed to GitHub.')}else{const idxFile=await ghApi(info.full);if(!idxFile){THREAD.list=[];await THREAD.save();alert('Remote is empty. Local list cleared.')}else{const remoteList=JSON.parse(btou(idxFile.content));THREAD.list=remoteList.map(t=>({...t,status:'synced'}));await THREAD.save();alert('Pulled from GitHub.')}}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}});