Update index.html

This commit is contained in:
sss
2025-08-14 17:41:16 -07:00
committed by GitHub
parent b74b476798
commit aa93c879bb

View File

@@ -133,8 +133,9 @@ el.sidebarBtn.addEventListener('click',openSidebar);
el.sidebarOverlay.addEventListener('click',()=>{closeSidebar();closeHistory();hideHistoryMenu()});
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';
async function ensureThreadOnFirstUser(text){if(state.currentThreadId)return;const id=gid(),now=Date.now();const th={id,title:titleFrom(text),pinned:false,createdAt:now,updatedAt:now,messages:[]};state.currentThreadId=id;threads.unshift(th);await idb.put(th);renderHistory()}
async function persistThread(){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId)||await idb.get(state.currentThreadId);if(!th)return;th.messages=[...state.messages];th.updatedAt=Date.now();th.title=titleFrom(th.messages.find(m=>m.role==='user')?.content||th.title);await idb.put(th);renderHistory()}
async function ensureThreadOnFirstUser(text){let needNew=!state.currentThreadId; if(state.messages.length===0) state.currentThreadId=null; if(state.currentThreadId){const existing=await idb.get(state.currentThreadId); if(!existing) needNew=true}
if(!needNew) return; const id=gid(),now=Date.now(); const th={id,title:titleFrom(text),pinned:false,createdAt:now,updatedAt:now,messages:[]}; state.currentThreadId=id; threads.unshift(th); await idb.put(th); await renderHistory()}
async function persistThread(){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId)||await idb.get(state.currentThreadId);if(!th)return;th.messages=[...state.messages];th.updatedAt=Date.now();th.title=titleFrom(th.messages.find(m=>m.role==='user')?.content||th.title);await idb.put(th);await renderHistory()}
function historyRow(t){return `<div class=\"relative flex items-center gap-2 px-3 py-2 ${t.pinned?'bg-yellow-50':''}\"><button data-open-thread=\"${t.id}\" class=\"flex-1 text-left truncate\">${t.pinned?'📌 ':''}${t.title}</button><button data-thread-menu=\"${t.id}\" class=\"h-7 w-7 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>`}
async function renderHistory(){threads=(await idb.all()).sort((a,b)=> (b.pinned-a.pinned)|| (b.updatedAt-a.updatedAt));el.historyList.innerHTML=threads.map(historyRow).join('');if(window.lucide)lucide.createIcons()}
function openHistory(){el.historyPanel.classList.remove('translate-x-full');el.historyOverlay.classList.remove('hidden');renderHistory()}
@@ -142,11 +143,11 @@ function closeHistory(){el.historyPanel.classList.add('translate-x-full');el.his
el.historyBtn.addEventListener('click',openHistory);el.historyOverlay.addEventListener('click',closeHistory);el.closeHistory.addEventListener('click',closeHistory);
let menuThreadId=null;function hideHistoryMenu(){el.historyMenu.classList.add('hidden');menuThreadId=null}
function showHistoryMenu(btn,id){menuThreadId=id;const r=btn.getBoundingClientRect();el.historyMenu.style.top=window.scrollY+r.bottom+4+'px';el.historyMenu.style.left=Math.min(window.innerWidth-220,window.scrollX+r.right-200)+'px';el.historyMenu.classList.remove('hidden');if(window.lucide)lucide.createIcons()}
el.historyList.addEventListener('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'),th=threads.find(t=>t.id===id);if(!th)return;state.currentThreadId=id;clearChat();const arr=[...(th.messages||[])];for(const m of arr)addMessage(m.role,m.content);state.messages=[...arr];queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));closeHistory();hideHistoryMenu();return}
el.historyList.addEventListener('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'),th=threads.find(t=>t.id===id)||await idb.get(id);if(!th)return;state.currentThreadId=id;clearChat();const arr=Array.isArray(th.messages)?[...th.messages]:[];for(const m of arr)addMessage(m.role,m.content);state.messages=[...arr];queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));closeHistory();hideHistoryMenu();return}
if(menuBtn){showHistoryMenu(menuBtn,menuBtn.getAttribute('data-thread-menu'))}});
document.addEventListener('click',e=>{if(!el.historyMenu.contains(e.target) && !e.target.closest('[data-thread-menu]'))hideHistoryMenu();if(!el.userMenu.contains(e.target) && !el.userMenuBtn.contains(e.target))el.userMenu.classList.add('hidden')});
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);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);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()}}}hideHistoryMenu();renderHistory()});
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text)return;await ensureThreadOnFirstUser(text);el.input.value='';el.input.style.height='auto';addMessage('user',text);state.busy=true;setBtnStop();const assistantBubble=addAssistantBubbleStreaming();let buf='';await askOpenRouterStreaming((delta,done)=>{buf+=delta;renderMarkdown(assistantBubble,buf);if(done){setBtnSend();state.busy=false;state.messages.push({role:'assistant',content:buf});persistThread();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}})});
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);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()}}}hideHistoryMenu();renderHistory()});
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='';el.input.style.height='auto';addMessage('user',text);state.busy=true;setBtnStop();const assistantBubble=addAssistantBubbleStreaming();let buf='';await askOpenRouterStreaming((delta,done)=>{buf+=delta;renderMarkdown(assistantBubble,buf);if(done){setBtnSend();state.busy=false;state.messages.push({role:'assistant',content:buf});persistThread();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}})});
el.messages.addEventListener('click',e=>{if(e.target.closest('.msg-avatar'))openSidebar()});
el.input.addEventListener('input',()=>{el.input.style.height='auto';el.input.style.height=Math.min(el.input.scrollHeight,160)+'px'});
function openSettings(){const a=getActive(),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_system_prompt.value=s.system_prompt;showModelTab();el.settingsModal.classList.remove('hidden')}
@@ -160,16 +161,16 @@ el.settingsModal.addEventListener('click',e=>{if(e.target===el.settingsModal||e.
el.tabModel.addEventListener('click',showModelTab);
el.tabPrompt.addEventListener('click',showPromptTab);
el.settingsForm.addEventListener('submit',e=>{e.preventDefault();const a=getActive(),s=a.settings;s.model=(el.set_model.value||DEFAULT_MODEL).trim();s.temperature=clamp(num(el.set_temperature.value,1.0),0,2);s.top_p=clamp(num(el.set_top_p.value,1.0),0,1);s.top_k=Math.max(0,int(el.set_top_k.value,0));s.frequency_penalty=clamp(num(el.set_frequency_penalty.value,0.0),-2,2);s.presence_penalty=clamp(num(el.set_presence_penalty.value,0.0),-2,2);s.repetition_penalty=clamp(num(el.set_repetition_penalty.value,1.0),0,2);s.min_p=clamp(num(el.set_min_p.value,0.0),0,1);s.top_a=clamp(num(el.set_top_a.value,0.0),0,1);s.system_prompt=el.set_system_prompt.value.trim();as.save(assistants);closeSettings();reflectActiveAssistant()});
el.deleteAssistantBtn.addEventListener('click',()=>{const activeId=as.getActiveId(),active=getActive(),name=active?.name||'this assistant';if(!confirm(`Delete "${name}"?`))return;assistants=assistants.filter(a=>a.id!==activeId);as.save(assistants);if(assistants.length===0){const def=createDefaultAssistant();assistants=[def];as.save(assistants);as.setActiveId(def.id)}else{as.setActiveId(assistants[0].id)}renderSidebar();reflectActiveAssistant();clearChat();closeSettings()});
el.deleteAssistantBtn.addEventListener('click',()=>{const activeId=as.getActiveId(),active=getActive(),name=active?.name||'this assistant';if(!confirm(`Delete "${name}"?`))return;assistants=assistants.filter(a=>a.id!==activeId);as.save(assistants);if(assistants.length===0){const def=createDefaultAssistant();assistants=[def];as.save(assistants);as.setActiveId(def.id)}else{as.setActiveId(assistants[0].id)}renderSidebar();reflectActiveAssistant();state.currentThreadId=null;clearChat();closeSettings()});
el.newAssistantBtn.addEventListener('click',()=>{const name=prompt('Name your assistant:');if(!name)return;const id=gid();assistants.unshift({id,name:name.trim(),settings:{model:DEFAULT_MODEL,temperature:1,top_p:1,top_k:0,frequency_penalty:0,presence_penalty:0,repetition_penalty:1,min_p:0,top_a:0,system_prompt:''}});as.save(assistants);as.setActiveId(id);renderSidebar();reflectActiveAssistant();state.currentThreadId=null;clearChat();closeSidebar()});
el.assistantList.addEventListener('click',e=>{const btn=e.target.closest('[data-asst-id]');if(!btn)return;const id=btn.getAttribute('data-asst-id');if(id){as.setActiveId(id);renderSidebar();reflectActiveAssistant();state.currentThreadId=null;clearChat();closeSidebar()}});
function toggleUserMenu(show){if(show===true)el.userMenu.classList.remove('hidden');else if(show===false)el.userMenu.classList.add('hidden');else el.userMenu.classList.toggle('hidden')}
el.userMenuBtn.addEventListener('click',e=>{e.stopPropagation();toggleUserMenu()});
el.apiKeyOption.addEventListener('click',()=>{toggleUserMenu(false);const cur=store.apiKey?'********':'';const input=prompt('Enter OpenRouter API key (stored locally):',cur);if(input!==null){store.apiKey=input==='********'?store.apiKey:input.trim();alert(store.apiKey?'API key saved locally.':'API key cleared.')}});
el.exportOption.addEventListener('click',()=>{const payload={version:1};const blob=new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='backup.json';document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url);toggleUserMenu(false)});
el.exportOption.addEventListener('click',()=>{const payload={version:1,assistants,activeId:as.getActiveId()};const blob=new Blob([JSON.stringify(payload,null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a'),ts=new Date(),pad=n=>String(n).padStart(2,'0'),fname=`backup-${ts.getFullYear()}${pad(ts.getMonth()+1)}${pad(ts.getDate())}-${pad(ts.getHours())}${pad(ts.getMinutes())}${pad(ts.getSeconds())}.json`;a.href=url;a.download=fname;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url);toggleUserMenu(false)});
el.importOption.addEventListener('click',()=>{el.importInput.value='';el.importInput.click()});
el.importInput.addEventListener('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{await file.text();alert('Imported (demo).');toggleUserMenu(false)}catch(err){alert('Import failed')}});
function init(){renderSidebar();reflectActiveAssistant();clearChat();if(window.lucide)lucide.createIcons()}
el.importInput.addEventListener('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const text=await file.text();JSON.parse(text);alert('Imported (demo).');toggleUserMenu(false)}catch(err){alert('Import failed')}});
async function init(){await idb.open();await renderHistory();renderSidebar();reflectActiveAssistant();clearChat();if(window.lucide)lucide.createIcons()}
window.addEventListener('resize',()=>hideHistoryMenu());
init()
</script>