mirror of
https://github.com/multipleof4/devsune.git
synced 2026-01-13 16:07:55 +00:00
Update index.html
This commit is contained in:
34
index.html
34
index.html
@@ -15,7 +15,7 @@
|
||||
.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}
|
||||
.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-card{position:fixed;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>
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
|
||||
<button id="sidebarBtn" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Assistants"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
|
||||
<button id="settingsBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Assistant settings">🤖</button>
|
||||
<div class="justify-self-end"><button id="historyBtn" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="History"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
|
||||
<div class="justify-self-end"><button id="historyBtn" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
|
||||
</div>
|
||||
</header>
|
||||
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar">
|
||||
@@ -50,15 +50,17 @@
|
||||
<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>
|
||||
<button id="assistantsImportOption" class="menu-item">Import assistants (.json)</button>
|
||||
<button id="assistantsExportOption" class="menu-item">Export assistants (.json)</button>
|
||||
<button id="threadsImportOption" class="menu-item">Import threads (.json)</button>
|
||||
<button id="threadsExportOption" class="menu-item">Export threads (.json)</button>
|
||||
</div>
|
||||
<input id="importInput" type="file" accept="application/json,.json" class="hidden"/>
|
||||
</div>
|
||||
</aside>
|
||||
<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">
|
||||
<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>Threads</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>
|
||||
</aside>
|
||||
<div id="historyMenu" class="menu-card hidden">
|
||||
@@ -66,7 +68,6 @@
|
||||
<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 -->
|
||||
<div id="settingsModal" class="hidden fixed inset-0 z-50">
|
||||
<div class="absolute inset-0 bg-black/30"></div>
|
||||
<div class="absolute inset-x-0 top-12 mx-auto w-full max-w-md px-4">
|
||||
@@ -103,7 +104,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
||||
<script>
|
||||
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','userMenuBtn','userMenu','apiKeyOption','importOption','exportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu'].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','assistantsImportOption','assistantsExportOption','threadsImportOption','threadsExportOption','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 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||'')}};
|
||||
@@ -136,15 +137,15 @@ let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)
|
||||
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 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-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>`}
|
||||
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 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);
|
||||
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()}
|
||||
function showHistoryMenu(btn,id){menuThreadId=id;const r=btn.getBoundingClientRect();el.historyMenu.style.top=(r.bottom+4)+'px';el.historyMenu.style.left=Math.min(window.innerWidth-220,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)||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'))}});
|
||||
if(menuBtn){e.stopPropagation();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)||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'}))}})});
|
||||
@@ -167,9 +168,14 @@ el.assistantList.addEventListener('click',e=>{const btn=e.target.closest('[data-
|
||||
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,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{const text=await file.text();JSON.parse(text);alert('Imported (demo).');toggleUserMenu(false)}catch(err){alert('Import failed')}});
|
||||
function dl(name,obj){const blob=new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
|
||||
function ts(){const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`}
|
||||
let importMode=null;
|
||||
el.assistantsExportOption.addEventListener('click',()=>{const payload={version:1,assistants,activeId:as.getActiveId()};dl(`assistants-${ts()}.json`,payload);toggleUserMenu(false)});
|
||||
el.assistantsImportOption.addEventListener('click',()=>{importMode='assistants';el.importInput.value='';el.importInput.click()});
|
||||
el.threadsExportOption.addEventListener('click',async()=>{const all=await idb.all();dl(`threads-${ts()}.json`,{version:1,threads:all});toggleUserMenu(false)});
|
||||
el.threadsImportOption.addEventListener('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()});
|
||||
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==='assistants'){const list=Array.isArray(data)?data:(Array.isArray(data.assistants)?data.assistants:[]);if(!list.length)throw new Error('No assistants');assistants=list.map(a=>({id:a.id||gid(),name:a.name||'Imported',settings:Object.assign({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:''},a.settings||{})}));as.save(assistants);as.setActiveId(data.activeId&&assistants.some(x=>x.id===data.activeId)?data.activeId:assistants[0]?.id||null);renderSidebar();reflectActiveAssistant();state.currentThreadId=null;clearChat();alert('Assistants imported.')}else if(importMode==='threads'){const arr=Array.isArray(data)?data:(Array.isArray(data.threads)?data.threads:[]);if(!arr.length)throw new Error('No threads');for(const t of arr){const id=gid();const th={id,title:titleFrom(t.title||titleFrom(t.messages?.find?.(m=>m.role==='user')?.content||'')),pinned:!!t.pinned,createdAt:t.createdAt||Date.now(),updatedAt:t.updatedAt||Date.now(),messages:Array.isArray(t.messages)?t.messages.filter(m=>m&&m.role&&m.content):[]};await idb.put(th)}await renderHistory();alert('Threads imported.')}toggleUserMenu(false)}catch(err){alert('Import failed')}finally{importMode=null}});
|
||||
async function init(){await idb.open();await renderHistory();renderSidebar();reflectActiveAssistant();clearChat();if(window.lucide)lucide.createIcons()}
|
||||
window.addEventListener('resize',()=>hideHistoryMenu());
|
||||
init()
|
||||
|
||||
Reference in New Issue
Block a user