mirror of
https://github.com/multipleof4/devsune.git
synced 2026-01-14 08:27:55 +00:00
Update index.html
This commit is contained in:
56
index.html
56
index.html
@@ -15,6 +15,9 @@
|
|||||||
.copy-btn{position:absolute;top:.5rem;right:.5rem;background:#0f172a;color:#fff;border-radius:.5rem;padding:.25rem .5rem;font-size:12px;opacity:.85}
|
.copy-btn{position:absolute;top:.5rem;right:.5rem;background:#0f172a;color:#fff;border-radius:.5rem;padding:.25rem .5rem;font-size:12px;opacity:.85}
|
||||||
.copy-btn:hover{opacity:1}
|
.copy-btn:hover{opacity:1}
|
||||||
.msg-avatar{font-size:16px}
|
.msg-avatar{font-size:16px}
|
||||||
|
.menu-card{position:absolute;z-index:60;min-width:12rem;border-radius:0.75rem;border:1px solid #e5e7eb;background:#fff;box-shadow:0 10px 20px rgba(0,0,0,.08)}
|
||||||
|
.menu-item{width:100%;text-align:left;padding:.5rem .75rem;font-size:.875rem;display:flex;align-items:center;gap:.5rem}
|
||||||
|
.menu-item:hover{background:#f9fafb}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white text-gray-900 selection:bg-black/10">
|
<body class="bg-white text-gray-900 selection:bg-black/10">
|
||||||
@@ -43,12 +46,26 @@
|
|||||||
<aside id="sidebar" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
<aside id="sidebar" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
||||||
<div class="p-3 border-b flex items-center gap-2"><button id="newAssistantBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New assistant</button><span class="text-xs text-gray-500">Click name to equip</span></div>
|
<div class="p-3 border-b flex items-center gap-2"><button id="newAssistantBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New assistant</button><span class="text-xs text-gray-500">Click name to equip</span></div>
|
||||||
<div id="assistantList" class="flex-1 overflow-y-auto divide-y"></div>
|
<div id="assistantList" class="flex-1 overflow-y-auto divide-y"></div>
|
||||||
|
<div class="p-3 border-t relative">
|
||||||
|
<button id="userMenuBtn" class="w-full flex items-center justify-between px-3 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition"><span class="flex items-center gap-2"><span class="h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center">👤</span><span class="text-sm">Account & Backup</span></span><i data-lucide="chevron-down" class="h-4 w-4"></i></button>
|
||||||
|
<div id="userMenu" class="absolute left-3 right-3 bottom-16 translate-y-2 rounded-xl border border-gray-200 bg-white shadow-lg hidden overflow-hidden">
|
||||||
|
<button id="apiKeyOption" class="menu-item">Enter OpenRouter API key</button>
|
||||||
|
<button id="importOption" class="menu-item">Import backup (.json)</button>
|
||||||
|
<button id="exportOption" class="menu-item">Export backup (.json)</button>
|
||||||
|
</div>
|
||||||
|
<input id="importInput" type="file" accept="application/json,.json" class="hidden"/>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<div id="historyOverlay" class="fixed inset-0 z-40 bg-black/20 hidden"></div>
|
<div id="historyOverlay" class="fixed inset-0 z-40 bg-black/20 hidden"></div>
|
||||||
<aside id="historyPanel" class="fixed inset-y-0 right-0 z-50 w-80 max-w-[90vw] bg-white border-l border-gray-200 shadow-xl transform translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
<aside id="historyPanel" class="fixed inset-y-0 right-0 z-50 w-80 max-w-[90vw] bg-white border-l border-gray-200 shadow-xl transform translate-x-full transition-transform duration-200 ease-out flex flex-col">
|
||||||
<div class="p-3 border-b text-sm font-medium flex items-center justify-between"><span>History</span><button id="closeHistory" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
|
<div class="p-3 border-b text-sm font-medium flex items-center justify-between"><span>History</span><button id="closeHistory" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
|
||||||
<div id="historyList" class="flex-1 overflow-y-auto divide-y"></div>
|
<div id="historyList" class="flex-1 overflow-y-auto divide-y"></div>
|
||||||
</aside>
|
</aside>
|
||||||
|
<div id="historyMenu" class="menu-card hidden">
|
||||||
|
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
|
||||||
|
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
|
||||||
|
<button data-action="delete" class="menu-item text-red-600"><i data-lucide="trash-2" class="h-4 w-4"></i><span>Delete</span></button>
|
||||||
|
</div>
|
||||||
<!-- Settings Modal -->
|
<!-- Settings Modal -->
|
||||||
<div id="settingsModal" class="hidden fixed inset-0 z-50">
|
<div id="settingsModal" class="hidden fixed inset-0 z-50">
|
||||||
<div class="absolute inset-0 bg-black/30"></div>
|
<div class="absolute inset-0 bg-black/30"></div>
|
||||||
@@ -86,13 +103,13 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const DEFAULT_MODEL='openai/gpt-4o',DEFAULT_API_KEY='';
|
const DEFAULT_MODEL='openai/gpt-4o',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_system_prompt','deleteAssistantBtn','sidebar','sidebarOverlay','sidebarBtn','assistantList','newAssistantBtn','historyBtn','historyPanel','historyOverlay','historyList','closeHistory'].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_system_prompt','deleteAssistantBtn','sidebar','sidebarOverlay','sidebarBtn','assistantList','newAssistantBtn','userMenuBtn','userMenu','apiKeyOption','importOption','exportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu'].map(id=>[id,document.getElementById(id)]));
|
||||||
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);
|
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);
|
||||||
const globalStore={get apiKey(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKey(v){localStorage.setItem('openrouter_api_key',v||'')}};
|
const globalStore={get apiKey(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKey(v){localStorage.setItem('openrouter_api_key',v||'')}};
|
||||||
const as={key:'assistants_v1',activeKey:'active_assistant_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 as={key:'assistants_v1',activeKey:'active_assistant_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||'')}};
|
||||||
let assistants=as.load();
|
let assistants=as.load();
|
||||||
if(!assistants.length){const def={id:gid(),name:'Default',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:''}};assistants=[def];as.save(assistants);as.setActiveId(def.id)}
|
if(!assistants.length){const def={id:gid(),name:'Default',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:''}};assistants=[def];as.save(assistants);as.setActiveId(def.id)}
|
||||||
const getActive=()=>assistants.find(a=>a.id===as.getActiveId())||assistants[0],setActive=id=>{as.setActiveId(id);renderSidebar();reflectActiveAssistant();clearChat()},createDefaultAssistant=()=>({id:gid(),name:'Default',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:''}});
|
const getActive=()=>assistants.find(a=>a.id===as.getActiveId())||assistants[0],createDefaultAssistant=()=>({id:gid(),name:'Default',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:''}});
|
||||||
const store=new Proxy({},{get(_,p){if(p==='apiKey')return globalStore.apiKey;const a=getActive();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=assistants.findIndex(a=>a.id===getActive().id);if(i>=0){if(p==='model')assistants[i].settings.model=v||DEFAULT_MODEL;else if(p==='system_prompt')assistants[i].settings.system_prompt=v||'';else assistants[i].settings[p]=v;as.save(assistants);return true}return false}});
|
const store=new Proxy({},{get(_,p){if(p==='apiKey')return globalStore.apiKey;const a=getActive();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=assistants.findIndex(a=>a.id===getActive().id);if(i>=0){if(p==='model')assistants[i].settings.model=v||DEFAULT_MODEL;else if(p==='system_prompt')assistants[i].settings.system_prompt=v||'';else assistants[i].settings[p]=v;as.save(assistants);return true}return false}});
|
||||||
const state={messages:[],busy:false,controller:null,currentThreadId:null};
|
const state={messages:[],busy:false,controller:null,currentThreadId:null};
|
||||||
const getModelShort=()=>{const m=store.model||'';return m.includes('/')?m.split('/').pop():m};
|
const getModelShort=()=>{const m=store.model||'';return m.includes('/')?m.split('/').pop():m};
|
||||||
@@ -113,23 +130,25 @@ function localDemoReply(prompt){const tips=['Tip: open the sidebar → Account &
|
|||||||
function openSidebar(){el.sidebar.classList.remove('-translate-x-full');el.sidebarOverlay.classList.remove('hidden')}
|
function openSidebar(){el.sidebar.classList.remove('-translate-x-full');el.sidebarOverlay.classList.remove('hidden')}
|
||||||
function closeSidebar(){el.sidebar.classList.add('-translate-x-full');el.sidebarOverlay.classList.add('hidden')}
|
function closeSidebar(){el.sidebar.classList.add('-translate-x-full');el.sidebarOverlay.classList.add('hidden')}
|
||||||
el.sidebarBtn.addEventListener('click',openSidebar);
|
el.sidebarBtn.addEventListener('click',openSidebar);
|
||||||
el.sidebarOverlay.addEventListener('click',()=>{closeSidebar();closeHistory()});
|
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)})}};
|
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';
|
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 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 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()}
|
||||||
function historyRow(t){return `<div class=\"group 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\"><svg viewBox=\"0 0 24 24\" class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"5\" cy=\"12\" r=\"1\"/><circle cx=\"12\" cy=\"12\" r=\"1\"/><circle cx=\"19\" cy=\"12\" r=\"1\"/></svg></button></div>`}
|
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('')}
|
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()}
|
function openHistory(){el.historyPanel.classList.remove('translate-x-full');el.historyOverlay.classList.remove('hidden');renderHistory()}
|
||||||
function closeHistory(){el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden')}
|
function closeHistory(){el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden')}
|
||||||
el.historyBtn.addEventListener('click',openHistory);el.historyOverlay.addEventListener('click',closeHistory);el.closeHistory.addEventListener('click',closeHistory);
|
el.historyBtn.addEventListener('click',openHistory);el.historyOverlay.addEventListener('click',closeHistory);el.closeHistory.addEventListener('click',closeHistory);
|
||||||
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();state.messages=[...th.messages];for(const m of state.messages)addMessage(m.role,m.content);queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));closeHistory();return}
|
let menuThreadId=null;function hideHistoryMenu(){el.historyMenu.classList.add('hidden');menuThreadId=null}
|
||||||
if(menuBtn){const id=menuBtn.getAttribute('data-thread-menu'),th=threads.find(t=>t.id===id);if(!th)return;const choice=prompt(`Actions for: ${th.title}\n(p) Pin/Unpin\n(r) Rename\n(d) Delete`,'');if(!choice)return;const c=choice.toLowerCase()[0];if(c==='p'){th.pinned=!th.pinned;await idb.put(th)}else if(c==='r'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);await idb.put(th)}}else if(c==='d'){if(confirm('Delete this chat?')){await idb.del(th.id);if(state.currentThreadId===th.id)state.currentThreadId=null}}await renderHistory();}
|
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}
|
||||||
|
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.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.messages.addEventListener('click',e=>{if(e.target.closest('.msg-avatar'))openSidebar()});
|
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'});
|
el.input.addEventListener('input',()=>{el.input.style.height='auto';el.input.style.height=Math.min(el.input.scrollHeight,160)+'px'});
|
||||||
// Settings wiring
|
|
||||||
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')}
|
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')}
|
||||||
function closeSettings(){el.settingsModal.classList.add('hidden')}
|
function 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')}
|
function showModelTab(){el.tabModel.classList.add('border-black');el.tabPrompt.classList.remove('border-black');el.panelModel.classList.remove('hidden');el.panelPrompt.classList.add('hidden')}
|
||||||
@@ -142,14 +161,17 @@ el.tabModel.addEventListener('click',showModelTab);
|
|||||||
el.tabPrompt.addEventListener('click',showPromptTab);
|
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.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();clearChat();closeSettings()});
|
||||||
// Assistant sidebar interactions
|
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.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();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()}});
|
||||||
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();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')}
|
||||||
// History
|
el.userMenuBtn.addEventListener('click',e=>{e.stopPropagation();toggleUserMenu()});
|
||||||
function openHistory(){el.historyPanel.classList.remove('translate-x-full');el.historyOverlay.classList.remove('hidden');renderHistory()}
|
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.')}});
|
||||||
function closeHistory(){el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden')}
|
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.historyBtn.addEventListener('click',openHistory);el.historyOverlay.addEventListener('click',closeHistory);el.closeHistory.addEventListener('click',closeHistory);
|
el.importOption.addEventListener('click',()=>{el.importInput.value='';el.importInput.click()});
|
||||||
async function init(){await idb.open();renderSidebar();reflectActiveAssistant();clearChat();renderHistory();if(window.lucide)lucide.createIcons()}init()
|
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()}
|
||||||
|
window.addEventListener('resize',()=>hideHistoryMenu());
|
||||||
|
init()
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user