mirror of
https://github.com/multipleof4/devsune.git
synced 2026-01-14 00:17:56 +00:00
Update index.html
This commit is contained in:
27
index.html
27
index.html
@@ -37,8 +37,9 @@
|
||||
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="false" autocapitalize="none" autocomplete="off" autocorrect="off" inputmode="text" enterkeyhint="enter" class="flex-1 resize-none rounded-2xl border-none bg-white px-3 py-2 text-[14px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-0 max-h-52 overflow-y-auto min-h-[96px]"></textarea>
|
||||
<div class="flex flex-col gap-2 self-stretch justify-center">
|
||||
<button id="sendBtn" type="submit" aria-label="Send" class="shrink-0 rounded-2xl bg-black text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-black/90 active:scale-[.98] transition"><i data-lucide="sparkles" class="h-5 w-5"></i></button>
|
||||
<button id="attachBtn" type="button" aria-label="Attach" class="shrink-0 rounded-2xl bg-gray-100 text-gray-900 h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-gray-200 active:scale-[.98] transition"><i data-lucide="paperclip" class="h-5 w-5"></i></button>
|
||||
<button id="attachBtn" type="button" aria-label="Attach" class="relative shrink-0 rounded-2xl bg-gray-100 text-gray-900 h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-gray-200 active:scale-[.98] transition"><i data-lucide="paperclip" class="h-5 w-5"></i><span id="attachBadge" class="hidden absolute -top-1 -right-1 h-4 min-w-4 px-1 rounded-full bg-black text-white text-[10px] leading-4 text-center"></span></button>
|
||||
</div>
|
||||
<input id="fileInput" type="file" class="hidden" multiple accept="image/png,image/jpeg,image/webp,image/gif,application/pdf,audio/wav,audio/x-wav,audio/mpeg,audio/mp3"/>
|
||||
</form>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -113,7 +114,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script>
|
||||
<script>
|
||||
const DEFAULT_MODEL='openai/gpt-5-chat',DEFAULT_API_KEY=''
|
||||
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','panelModel','panelPrompt','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_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer'].map(id=>[id,document.getElementById(id)]))
|
||||
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','panelModel','panelPrompt','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_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput'].map(id=>[id,document.getElementById(id)]))
|
||||
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=>({"&":"&","<":"<",">":">","\"":""","'":"'","`":"`"}[c]))
|
||||
const globalStore={get apiKey(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKey(v){localStorage.setItem('openrouter_api_key',v||'')}}
|
||||
@@ -124,7 +125,7 @@ let sunes=(su.load()||[]).map(makeSune)
|
||||
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 store=new Proxy({},{get(_,p){if(p==='apiKey')return globalStore.apiKey;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==='apiKey'){globalStore.apiKey=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}
|
||||
const state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[]}
|
||||
const getModelShort=m=>{const mm=m||store.model||'';return mm.includes('/')?mm.split('/').pop():mm}
|
||||
const reflectActiveSune=()=>{const a=getActiveSune();el.settingsBtnTop.title=`Settings — ${a.name}`;el.settingsBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺';icons()}
|
||||
const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===su.getActiveId()?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-6 w-6 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.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>`
|
||||
@@ -140,7 +141,8 @@ const clearChat=()=>{state.messages=[];el.messages.innerHTML=''}
|
||||
const payloadWithSampling=b=>Object.assign({},b,{temperature:store.temperature,top_p:store.top_p,top_k:store.top_k,frequency_penalty:store.frequency_penalty,presence_penalty:store.presence_penalty,repetition_penalty:store.repetition_penalty,min_p:store.min_p,top_a:store.top_a})
|
||||
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.()}}
|
||||
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
|
||||
async function askOpenRouterStreaming(onDelta){const apiKey=store.apiKey,model=store.model;if(!apiKey){const text=localDemoReply(state.messages[state.messages.length-1]?.content||'');onDelta(text,true);return {ok:true,text}}try{state.controller=new AbortController();const msgs=[];if(store.system_prompt)msgs.push({role:'system',content:store.system_prompt});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));let body=payloadWithSampling({model,messages:msgs,stream:true});const re=store.reasoning_effort; if(re&&re!=='default')body.reasoning={effort:re};const res=await fetch('https://openrouter.ai/api/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body:JSON.stringify(body),signal:state.controller.signal});if(!res.ok){const errText=await res.text().catch(()=> '');throw new Error(errText||('HTTP '+res.status))}const reader=res.body.getReader(),decoder=new TextDecoder('utf-8');let buffer='',full='',finished=false;const doneOnce=()=>{if(finished)return;finished=true;onDelta('',true)};while(true){const {value,done}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});let idx;while((idx=buffer.indexOf('\n\n'))!==-1){const chunk=buffer.slice(0,idx).trim();buffer=buffer.slice(idx+2);if(!chunk)continue;if(chunk.startsWith('data:')){const data=chunk.slice(5).trim();if(data==='[DONE]'){doneOnce();continue}try{const json=JSON.parse(data);const delta=json.choices?.[0]?.delta?.content??'';if(delta){full+=delta;onDelta(delta,false)}const finish=json.choices?.[0]?.finish_reason;if(finish)doneOnce()}catch{}}}}doneOnce();return {ok:true,text:full}}catch(e){const msg=String(e?.message||e),aborted=e?.name==='AbortError'||/abort/i.test(msg)||state.controller?.signal?.aborted||state.abortRequested;if(aborted){onDelta('',true);return {ok:false,text:'',aborted:true}}let hint='Request failed.';if(/401|unauthorized/i.test(msg))hint='Unauthorized (check API key).';else if(/429|rate/i.test(msg))hint='Rate limited (slow down or upgrade).';else if(/access|forbidden|403/i.test(msg))hint='Forbidden (model or key scope).';const fallback='\n\n'+hint;onDelta(fallback,true);return {ok:false,text:fallback}}finally{state.controller=null;state.abortRequested=false}}
|
||||
async function askOpenRouterStreaming(onDelta){const apiKey=store.apiKey,model=store.model;if(!apiKey){const text=localDemoReply(state.messages[state.messages.length-1]?.content||'');onDelta(text,true);return {ok:true,text}}try{state.controller=new AbortController();const msgs=[];if(store.system_prompt)msgs.push({role:'system',content:store.system_prompt});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.contentParts||m.content})));
|
||||
let body=payloadWithSampling({model,messages:msgs,stream:true});const re=store.reasoning_effort; if(re&&re!=='default')body.reasoning={effort:re};const res=await fetch('https://openrouter.ai/api/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+apiKey},body:JSON.stringify(body),signal:state.controller.signal});if(!res.ok){const errText=await res.text().catch(()=> '');throw new Error(errText||('HTTP '+res.status))}const reader=res.body.getReader(),decoder=new TextDecoder('utf-8');let buffer='',full='',finished=false;const doneOnce=()=>{if(finished)return;finished=true;onDelta('',true)};while(true){const {value,done}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});let idx;while((idx=buffer.indexOf('\n\n'))!==-1){const chunk=buffer.slice(0,idx).trim();buffer=buffer.slice(idx+2);if(!chunk)continue;if(chunk.startsWith('data:')){const data=chunk.slice(5).trim();if(data==='[DONE]'){doneOnce();continue}try{const json=JSON.parse(data);const delta=json.choices?.[0]?.delta?.content??'';if(delta){full+=delta;onDelta(delta,false)}const finish=json.choices?.[0]?.finish_reason;if(finish)doneOnce()}catch{}}}}doneOnce();return {ok:true,text:full}}catch(e){const msg=String(e?.message||e),aborted=e?.name==='AbortError'||/abort/i.test(msg)||state.controller?.signal?.aborted||state.abortRequested;if(aborted){onDelta('',true);return {ok:false,text:'',aborted:true}}let hint='Request failed.';if(/401|unauthorized/i.test(msg))hint='Unauthorized (check API key).';else if(/429|rate/i.test(msg))hint='Rate limited (slow down or upgrade).';else if(/access|forbidden|403/i.test(msg))hint='Forbidden (model or key scope).';const fallback='\n\n'+hint;onDelta(fallback,true);return {ok:false,text:fallback}}finally{state.controller=null;state.abortRequested=false}}
|
||||
function localDemoReply(){return 'Tip: open the sidebar → Account & Backup to set your OpenRouter API key.'}
|
||||
const idb={db:null,open(){return new Promise((res,rej)=>{const r=indexedDB.open('chat_history_v1',1);r.onupgradeneeded=()=>{r.result.createObjectStore('threads',{keyPath:'id'})};r.onsuccess=()=>{this.db=r.result;res()};r.onerror=()=>rej(r.error)})},all(){return new Promise((res,rej)=>{const tx=this.db.transaction('threads').objectStore('threads').getAll();tx.onsuccess=()=>res(tx.result||[]);tx.onerror=()=>rej(tx.error)})},get(id){return new Promise((res,rej)=>{const tx=this.db.transaction('threads').objectStore('threads').get(id);tx.onsuccess=()=>res(tx.result||null);tx.onerror=()=>rej(tx.error)})},put(v){return new Promise((res,rej)=>{const tx=this.db.transaction('threads','readwrite').objectStore('threads').put(v);tx.onsuccess=()=>res();tx.onerror=()=>rej(tx.error)})},del(id){return new Promise((res,rej)=>{const tx=this.db.transaction('threads','readwrite').objectStore('threads').delete(id);tx.onsuccess=()=>res();tx.onerror=()=>rej(tx.error)})}}
|
||||
let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled'
|
||||
@@ -156,13 +158,14 @@ el.historyList.addEventListener('click',async e=>{const openBtn=e.target.closest
|
||||
el.historyMenu.addEventListener('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=threads.find(t=>t.id===menuThreadId)||await idb.get(menuThreadId);if(!th)return;if(act==='pin'){th.pinned=!th.pinned;await idb.put(th)}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();await idb.put(th)}}else if(act==='delete'){if(confirm('Delete this chat?')){await idb.del(th.id);if(state.currentThreadId===th.id){state.currentThreadId=null;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(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+')')}hideHistoryMenu();renderHistory()})
|
||||
el.suneList.addEventListener('click',e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSuneMenu(menuBtn,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){su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')}})
|
||||
el.suneMenu.addEventListener('click',e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=sunes.find(x=>x.id===menuSuneId);if(!s)return;if(act==='pin')s.pinned=!s.pinned;else if(act==='rename'){const nv=prompt('Rename sune to:',s.name);if(nv!=null)s.name=nv.trim()||s.name}else if(act==='pfp'){const url=prompt('Image URL:',s.avatar||'');if(url!==null)s.avatar=url.trim()}s.updatedAt=Date.now();su.save(sunes);hideSuneMenu();renderSidebar();reflectActiveSune()})
|
||||
const raf=(fn=>{let id=null;return()=>{if(id)cancelAnimationFrame(id);id=requestAnimationFrame(()=>{id=null;fn()})}})
|
||||
const big=()=>el.input.value.length>50||el.input.value.split('\n').length>4
|
||||
const fit=()=>{const large=big();el.input.style.overflowY=large?'auto':'hidden';el.input.style.height='auto';const cap=200,min=96;const h=Math.min(el.input.scrollHeight,cap);el.input.style.height=Math.max(min,h)+'px'}
|
||||
const fitRAF=raf(fit)
|
||||
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text);el.input.value='';fit();addMessage({role:'user',content:text});state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);let buf='',completed=false;await askOpenRouterStreaming((delta,done)=>{buf+=delta;renderMarkdown(suneBubble,buf,{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push({role:'assistant',content:buf,...suneMeta});persistThread();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}})})
|
||||
el.input.addEventListener('input',fitRAF)
|
||||
el.input.addEventListener('paste',()=>setTimeout(fit,0))
|
||||
function updateAttachBadge(){const n=state.attachments.length;el.attachBadge.textContent=String(n);el.attachBadge.classList.toggle('hidden',n===0)}
|
||||
async function fileToPart(file){const name=file.name||'file',type=(file.type||'').toLowerCase();const asDataURL=f=>new Promise(r=>{const fr=new FileReader();fr.onload=()=>r(String(fr.result||''));fr.readAsDataURL(f)});if(type.startsWith('image/')||/\.(png|jpe?g|webp|gif)$/i.test(name)){const url=await asDataURL(file);return {label:name,part:{type:'image_url',image_url:{url:url}}}}
|
||||
if(type==='application/pdf'||/\.pdf$/i.test(name)){const dataUrl=await asDataURL(file);const base64=dataUrl.split(',')[1]||'';return {label:name,part:{type:'file',file:{filename:name,file_data:base64}}}}
|
||||
if(type.startsWith('audio/')||/\.(wav|mp3)$/i.test(name)){const dataUrl=await asDataURL(file);const base64=dataUrl.split(',')[1]||'';let fmt='wav';if(/mp3/.test(type)||/\.mp3$/i.test(name))fmt='mp3';else if(/wav/.test(type)||/\.wav$/i.test(name))fmt='wav';return {label:name,part:{type:'input_audio',input_audio:{data:base64,format:fmt}}}}
|
||||
return null}
|
||||
el.attachBtn.addEventListener('click',()=>{if(state.busy)return;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 part=await fileToPart(f).catch(()=>null);if(part)state.attachments.push(part)}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 names=state.attachments.map(a=>a.label).join(', ');const display=text+(names?`\n\n**Attachments:** ${esc(names)}`:'');const contentParts=[];if(text)contentParts.push({type:'text',text});state.attachments.forEach(a=>contentParts.push(a.part));addMessage({role:'user',content:display,contentParts});state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);let buf='',completed=false;await askOpenRouterStreaming((delta,done)=>{buf+=delta;renderMarkdown(suneBubble,buf,{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push({role:'assistant',content:buf,...suneMeta});persistThread();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}});state.attachments=[];updateAttachBadge()})
|
||||
function openSettings(){const a=getActiveSune(),s=a.settings;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_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;showModelTab();el.settingsModal.classList.remove('hidden')}
|
||||
const closeSettings=()=>{el.settingsModal.classList.add('hidden')}
|
||||
function showModelTab(){el.tabModel.classList.add('border-black');el.tabPrompt.classList.remove('border-black');el.panelModel.classList.remove('hidden');el.panelPrompt.classList.add('hidden')}
|
||||
@@ -187,7 +190,7 @@ el.threadsImportOption.addEventListener('click',()=>{importMode='threads';el.imp
|
||||
el.importInput.addEventListener('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const text=await file.text();const data=JSON.parse(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||{}));const 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(sunes.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex){sunes.push(s);added++}else if(+s.updatedAt>+ex.updatedAt){Object.assign(ex,s);updated++}});su.save(sunes);if(data.activeId&&sunes.some(x=>x.id===data.activeId))su.setActiveId(data.activeId);renderSidebar();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;for(const th of Object.values(best)){const ex=await idb.get(th.id);if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}await idb.put(th);kept++}await renderHistory();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
|
||||
function kbUpdate(){const vv=window.visualViewport;const overlap=vv?Math.max(0,(window.innerHeight-(vv.height+vv.offsetTop))):0;document.documentElement.style.setProperty('--kb',overlap+'px');const fh=el.footer.getBoundingClientRect().height;document.documentElement.style.setProperty('--footer-h',fh+'px');el.footer.style.transform='translateY('+(-overlap)+'px)';el.chat.style.scrollPaddingBottom=(fh+overlap+16)+'px'}
|
||||
function kbBind(){if(window.visualViewport){['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,()=>kbUpdate(),{passive:true}))}['resize','orientationchange'].forEach(ev=>window.addEventListener(ev,()=>setTimeout(kbUpdate,50),{passive:true}));['focus','click'].forEach(ev=>el.input.addEventListener(ev,()=>{setTimeout(()=>{kbUpdate();el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)}))}
|
||||
async function init(){await idb.open();await renderHistory();renderSidebar();reflectActiveSune();clearChat();icons();fit();kbBind();kbUpdate()}
|
||||
async function init(){await idb.open();await renderHistory();renderSidebar();reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
|
||||
window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()})
|
||||
init()
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user