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:
96
index.html
96
index.html
@@ -18,10 +18,6 @@
|
||||
.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}
|
||||
.drop-hint{position:relative}
|
||||
.drop-hint:before{content:"";position:absolute;left:8px;right:8px;height:2px;background:#111827}
|
||||
.drop-top:before{top:-1px}
|
||||
.drop-bottom:before{bottom:-1px}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-white text-gray-900 selection:bg-black/10">
|
||||
@@ -45,8 +41,15 @@
|
||||
</div>
|
||||
<div id="sidebarOverlay" class="fixed inset-0 z-40 bg-black/20 hidden"></div>
|
||||
<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="newSuneBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New sune</button><button id="newFolderBtn" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Add folder"><i data-lucide="folder-plus" class="h-4 w-4"></i></button></div>
|
||||
<div id="suneList" class="flex-1 overflow-y-auto"></div>
|
||||
<div class="p-3 border-b flex items-center gap-2">
|
||||
<button id="newSuneBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New sune</button>
|
||||
<button id="newFolderBtn" class="h-9 w-9 rounded-xl bg-gray-900/90 text-white hover:bg-gray-900 active:scale-[.99] transition flex items-center justify-center" title="New folder"><i data-lucide="folder-plus" class="h-4 w-4"></i></button>
|
||||
</div>
|
||||
<div id="suneList" class="flex-1 overflow-y-auto">
|
||||
<div id="rootFolders" class="p-2 space-y-2"></div>
|
||||
<div class="border-t my-2"></div>
|
||||
<div id="rootSunes" class="p-1 space-y-0"></div>
|
||||
</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">
|
||||
@@ -62,7 +65,7 @@
|
||||
</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>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 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 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">
|
||||
@@ -71,7 +74,7 @@
|
||||
<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>
|
||||
<div id="folderMenu" class="menu-card hidden">
|
||||
<button data-act="delete-folder" class="menu-item text-red-600"><i data-lucide="trash-2" class="h-4 w-4"></i><span>Delete folder</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 folder</span></button>
|
||||
</div>
|
||||
<div id="settingsModal" class="hidden fixed inset-0 z-50">
|
||||
<div class="absolute inset-0 bg-black/30"></div>
|
||||
@@ -107,34 +110,51 @@
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.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','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','newFolderBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','threadsDedupOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','folderMenu','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_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','newFolderBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','threadsDedupOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','folderMenu','footer','rootFolders','rootSunes'].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),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||'')}}
|
||||
const su={key:'sunes_v1',activeKey:'active_sune_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 sunes=su.load()
|
||||
if(!sunes.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:''}};sunes=[def];su.save(sunes);su.setActiveId(def.id)}
|
||||
const fo={key:'sune_folders_v1',openKey:'sune_folder_open',load(){try{return JSON.parse(localStorage.getItem(this.key)||'')||{folders:[],order:{}}}catch{return{folders:[],order:{}}}},save(v){localStorage.setItem(this.key,JSON.stringify(v||{folders:[],order:{}}))},getOpen(){try{return new Set(JSON.parse(localStorage.getItem(this.openKey)||'[]'))}catch{return new Set()}},setOpen(set){localStorage.setItem(this.openKey,JSON.stringify([...set]))}}
|
||||
let foldersData=fo.load();if(!foldersData.order.root){foldersData.order.root=sunes.map(s=>s.id);fo.save(foldersData)}
|
||||
const getFolder=(id)=>foldersData.folders.find(f=>f.id===id)||null
|
||||
const getOrder=(fid)=>foldersData.order[fid||'root']||[]
|
||||
const setOrder=(fid,arr)=>{foldersData.order[fid||'root']=arr;fo.save(foldersData)}
|
||||
const folderOpen=fo.getOpen()
|
||||
const getActiveSune=()=>sunes.find(a=>a.id===su.getActiveId())||sunes[0],createDefaultSune=()=>({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 fo={key:'sune_folders_v1',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))}}
|
||||
let sunes=su.load().map(a=>Object.assign({order:0,folderId:null},a)),folders=fo.load().map(f=>Object.assign({open:true,order:0},f))
|
||||
if(!sunes.length){const def={id:gid(),name:'Default',order:0,folderId:null,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:''}};sunes=[def];su.save(sunes);su.setActiveId(def.id)}
|
||||
const getActiveSune=()=>sunes.find(a=>a.id===su.getActiveId())||sunes[0],createDefaultSune=()=>({id:gid(),name:'Default',order:0,folderId:null,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=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;su.save(sunes);return true}return false}})
|
||||
const state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false}
|
||||
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}`;if(window.lucide)lucide.createIcons()}
|
||||
const rowHTML=(id,active,icon,label,extra='')=>`<div class="group flex items-center" data-row id="row-${id}"><button data-sune-id="${id}" draggable="true" class="flex-1 text-left px-3 py-2 hover:bg-gray-50 flex items-center gap-2 ${active?'bg-gray-100':''}"><span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">🤖</span><span class="truncate">${esc(label)}</span></button>${extra}</div>`
|
||||
const folderRowHTML=(f)=>{const open=folderOpen.has(f.id),chev=open?'chevron-down':'chevron-right',ico=open?'folder-open':'folder';return `<div class="border-b" data-folder-block="${f.id}"><div class="flex items-center"><button data-folder-toggle="${f.id}" draggable="true" class="flex-1 text-left px-3 py-2 hover:bg-gray-50 flex items-center gap-2"><i data-lucide="${ico}" class="h-4 w-4"></i><span class="truncate font-medium">${esc(f.name)}</span></button><button data-folder-menu="${f.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><div data-folder-list="${f.id}" class="ml-4 ${open?'':'hidden'}"></div></div>`}
|
||||
const renderSidebar=()=>{const activeId=su.getActiveId();const rootOrder=getOrder('root').filter(id=>sunes.some(s=>s.id===id));const orphan=sunes.map(s=>s.id).filter(id=>!rootOrder.includes(id)&&!Object.values(foldersData.order).some(arr=>arr.includes(id)));if(orphan.length){setOrder('root',[...rootOrder,...orphan])}
|
||||
const blocks=[...foldersData.folders.map(folderRowHTML),`<div data-root-list class=""> </div>`];el.suneList.innerHTML=blocks.join('');const root=el.suneList.querySelector('[data-root-list]');root.innerHTML=getOrder('root').map(id=>{const s=sunes.find(x=>x.id===id);return s?rowHTML(s.id,s.id===activeId,'bot',s.name):''}).join('');foldersData.folders.forEach(f=>{const cont=el.suneList.querySelector(`[data-folder-list="${f.id}"]`);cont.innerHTML=getOrder(f.id).map(id=>{const s=sunes.find(x=>x.id===id);return s?rowHTML(s.id,s.id===activeId,'bot',s.name):''}).join('')});if(window.lucide)lucide.createIcons();bindDND()}
|
||||
function enhanceCodeBlocks(root,doHL=true){root.querySelectorAll('pre>code').forEach(code=>{if(code.textContent.length>200000)return;const pre=code.parentElement;pre.classList.add('relative','rounded-xl','border','border-gray-200');if(!pre.querySelector('.copy-btn')){const btn=document.createElement('button');btn.className='copy-btn';btn.textContent='Copy';btn.addEventListener('click',async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(code.innerText);btn.textContent='Copied';setTimeout(()=>btn.textContent='Copy',1200)}catch{}});pre.appendChild(btn)}if(doHL&&window.hljs&&code.textContent.length<100000)hljs.highlightElement(code)})}
|
||||
const suneBtn=t=>`<button data-sune-id="${t.id}" class="s-item w-full text-left px-3 py-2 hover:bg-gray-50 flex items-center gap-2 ${t.id===su.getActiveId()?'bg-gray-100':''}"><span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">🤖</span><span class="truncate">${esc(t.name)}</span></button>`
|
||||
const folderBlock=f=>{
|
||||
const open=!!f.open,chev=open?'chevron-down':'chevron-right'
|
||||
const innerId=`folder_${f.id}`
|
||||
return `<div class="f-item border rounded-xl" data-folder-id="${f.id}">
|
||||
<div class="flex items-center justify-between px-2 py-1.5 select-none cursor-pointer" data-folder-toggle="${f.id}">
|
||||
<div class="flex items-center gap-2"><i data-lucide="${chev}" class="h-4 w-4 text-gray-600"></i><i data-lucide="folder" class="h-4 w-4 text-gray-700"></i><span class="text-sm font-medium truncate">${esc(f.name)}</span><span class="text-xs text-gray-400 ml-1">(${sunes.filter(s=>s.folderId===f.id).length})</span></div>
|
||||
<button class="p-1 rounded hover:bg-gray-100" data-folder-menu="${f.id}"><i data-lucide="more-horizontal" class="h-4 w-4"></i></button>
|
||||
</div>
|
||||
<div id="${innerId}" class="suneGroup ${open?'':'hidden'} px-1 pb-2 space-y-0"></div>
|
||||
</div>`
|
||||
}
|
||||
function renderSidebar(){const activeId=su.getActiveId();folders.sort((a,b)=>a.order-b.order);sunes.sort((a,b)=>a.order-b.order);el.rootFolders.innerHTML=folders.map(folderBlock).join('');el.rootSunes.innerHTML=sunes.filter(s=>!s.folderId).map(suneBtn).join('');folders.forEach(f=>{const wrap=document.getElementById('folder_'+f.id);if(wrap)wrap.innerHTML=sunes.filter(s=>s.folderId===f.id).map(suneBtn).join('')});initDnD();if(window.lucide)lucide.createIcons()}
|
||||
function setOrders(list,ids,folderId){ids.forEach((id,i)=>{const s=sunes.find(x=>x.id===id);if(s){s.order=i;s.folderId=folderId||null}});su.save(sunes)}
|
||||
let sortablesInited=false
|
||||
function initDnD(){if(sortablesInited){return}sortablesInited=true
|
||||
new Sortable(el.rootFolders,{animation:150,draggable:'.f-item',handle:'[data-folder-toggle]',ghostClass:'bg-gray-50',delayOnTouchOnly:true,delay:150,onEnd:e=>{Array.from(el.rootFolders.querySelectorAll('.f-item')).forEach((node,i)=>{const id=node.getAttribute('data-folder-id');const f=folders.find(x=>x.id===id);if(f)f.order=i});fo.save(folders)}})
|
||||
const makeGroup=tgt=>new Sortable(tgt,{group:'sunes',animation:150,draggable:'.s-item',ghostClass:'bg-gray-50',delayOnTouchOnly:true,delay:150,onEnd:e=>{
|
||||
const isRoot=tgt===el.rootSunes;const folderId=isRoot?null:tgt.getAttribute('data-folder');const ids=Array.from(tgt.querySelectorAll('.s-item')).map(x=>x.getAttribute('data-sune-id'));setOrders(tgt,ids,folderId)
|
||||
},onAdd:e=>{
|
||||
const p=e.to;const isRoot=p===el.rootSunes;const folderId=isRoot?null:p.getAttribute('data-folder');const ids=Array.from(p.querySelectorAll('.s-item')).map(x=>x.getAttribute('data-sune-id'));setOrders(p,ids,folderId)
|
||||
}})
|
||||
makeGroup(el.rootSunes)
|
||||
folders.forEach(f=>{const inner=document.getElementById('folder_'+f.id).parentElement.querySelector('.suneGroup');inner.setAttribute('data-folder',f.id);makeGroup(inner)})
|
||||
}
|
||||
const md=window.markdownit({html:false,linkify:true,typographer:true,breaks:true})
|
||||
const getSuneLabel=m=>{const name=(m&&m.sune_name)||getActiveSune().name,modelShort=getModelShort(m&&m.model);return `${name} · ${modelShort}`}
|
||||
function msgRow(m){const role=typeof m==='string'?m:(m&&m.role)||'assistant';const meta=typeof m==='string'?{}:m||{};const row=document.createElement('div');row.className='flex flex-col gap-2';const head=document.createElement('div');head.className='flex items-center gap-2 px-4';const avatar=document.createElement('div');avatar.className=(role==='user'?'bg-gray-900 text-white':'bg-gray-200 text-gray-900')+' msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center';avatar.textContent=role==='user'?'🧑':'🤖';const name=document.createElement('div');name.className='text-xs font-medium text-gray-500';name.textContent=role==='user'?'You':getSuneLabel(meta);head.appendChild(avatar);head.appendChild(name);const bubble=document.createElement('div');bubble.className=(role==='user'?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full';row.appendChild(head);row.appendChild(bubble);el.messages.appendChild(row);queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));return bubble}
|
||||
function renderMarkdown(node,text,opt={enhance:true,highlight:true}){node.innerHTML=md.render(text);if(opt.enhance)enhanceCodeBlocks(node,opt.highlight)}
|
||||
function enhanceCodeBlocks(root,doHL=true){root.querySelectorAll('pre>code').forEach(code=>{if(code.textContent.length>200000)return;const pre=code.parentElement;pre.classList.add('relative','rounded-xl','border','border-gray-200');if(!pre.querySelector('.copy-btn')){const btn=document.createElement('button');btn.className='copy-btn';btn.textContent='Copy';btn.addEventListener('click',async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(code.innerText);btn.textContent='Copied';setTimeout(()=>btn.textContent='Copy',1200)}catch{}});pre.appendChild(btn)}if(doHL&&window.hljs&&code.textContent.length<100000)hljs.highlightElement(code)})}
|
||||
function addMessage(m,track=true){const bubble=msgRow(m);renderMarkdown(bubble,m.content);if(track)state.messages.push(m);return bubble}
|
||||
const addSuneBubbleStreaming=meta=>msgRow(Object.assign({role:'assistant'},meta))
|
||||
const clearChat=()=>{state.messages=[];el.messages.innerHTML=''}
|
||||
@@ -159,7 +179,7 @@ el.historyBtn.addEventListener('click',openHistory);el.historyOverlay.addEventLi
|
||||
let menuThreadId=null;const hideHistoryMenu=()=>{el.historyMenu.classList.add('hidden');menuThreadId=null}
|
||||
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();state.messages=Array.isArray(th.messages)?[...th.messages]:[];for(const m of state.messages){const b=msgRow(m);renderMarkdown(b,m.content)}queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));closeHistory();hideHistoryMenu();return}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');if(!el.folderMenu.contains(e.target)&&!e.target.closest('[data-folder-menu]'))hideFolderMenu()})
|
||||
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()})
|
||||
const raf=(fn=>{let id=null;return()=>{if(id)cancelAnimationFrame(id);id=requestAnimationFrame(()=>{id=null;fn()})}})
|
||||
const big=()=>el.input.value.length>5000||el.input.value.split('\n').length>200
|
||||
@@ -180,34 +200,30 @@ 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=getActiveSune(),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();su.save(sunes);closeSettings();reflectActiveSune()})
|
||||
el.deleteSuneBtn.addEventListener('click',()=>{const activeId=su.getActiveId(),active=getActiveSune(),name=active?.name||'this sune';if(!confirm(`Delete "${name}"?`))return;const idx=sunes.findIndex(a=>a.id===activeId);if(idx<0)return;sunes=sunes.filter(a=>a.id!==activeId);Object.keys(foldersData.order).forEach(k=>{foldersData.order[k]=foldersData.order[k].filter(id=>id!==activeId)});fo.save(foldersData);su.save(sunes);if(sunes.length===0){const def=createDefaultSune();sunes=[def];su.save(sunes);su.setActiveId(def.id);if(!foldersData.order.root)foldersData.order.root=[];foldersData.order.root.unshift(def.id);fo.save(foldersData)}else{const root=getOrder('root');if(!root.includes(sunes[0].id)){root.unshift(sunes[0].id);setOrder('root',root)}su.setActiveId(sunes[0].id)}renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
|
||||
el.newSuneBtn.addEventListener('click',()=>{const name=prompt('Name your sune:');if(!name)return;const id=gid();sunes.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:''}});su.save(sunes);su.setActiveId(id);const order=getOrder('root');setOrder('root',[id,...order.filter(x=>x!==id)]);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSidebar()})
|
||||
el.newFolderBtn.addEventListener('click',()=>{const name=prompt('Folder name:');if(!name)return;const f={id:gid(),name:name.trim()};foldersData.folders.push(f);foldersData.order[f.id]=foldersData.order[f.id]||[];fo.save(foldersData);folderOpen.add(f.id);fo.setOpen(folderOpen);renderSidebar()})
|
||||
el.suneList.addEventListener('click',e=>{const sbtn=e.target.closest('[data-sune-id]');if(sbtn){const id=sbtn.getAttribute('data-sune-id');if(id){su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSidebar();return}}const ft=e.target.closest('[data-folder-toggle]');if(ft){const id=ft.getAttribute('data-folder-toggle');if(folderOpen.has(id))folderOpen.delete(id);else folderOpen.add(id);fo.setOpen(folderOpen);const blk=el.suneList.querySelector(`[data-folder-block="${id}"]`);if(blk){const list=blk.querySelector(`[data-folder-list="${id}"]`);list.classList.toggle('hidden');const i=ft.querySelector('[data-lucide]');if(i){i.setAttribute('data-lucide',list.classList.contains('hidden')?'folder':'folder-open');lucide.createIcons()}return}}const fm=e.target.closest('[data-folder-menu]');if(fm){e.stopPropagation();showFolderMenu(fm,fm.getAttribute('data-folder-menu'))}})
|
||||
el.deleteSuneBtn.addEventListener('click',()=>{const activeId=su.getActiveId(),active=getActiveSune(),name=active?.name||'this sune';if(!confirm(`Delete "${name}"?`))return;sunes=sunes.filter(a=>a.id!==activeId);su.save(sunes);if(sunes.length===0){const def=createDefaultSune();sunes=[def];su.save(sunes);su.setActiveId(def.id)}else{su.setActiveId(sunes[0].id)}renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
|
||||
el.newSuneBtn.addEventListener('click',()=>{const name=prompt('Name your sune:');if(!name)return;const id=gid();sunes.unshift({id,name:name.trim(),order:0,folderId:null,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:''}});sunes.forEach((s,i)=>s.order=i);su.save(sunes);su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSidebar()})
|
||||
el.newFolderBtn.addEventListener('click',()=>{const name=prompt('Folder name:');if(!name)return;const id=gid();folders.push({id,name:name.trim(),open:true,order:folders.length});fo.save(folders);renderSidebar()})
|
||||
el.suneList.addEventListener('click',e=>{const btn=e.target.closest('[data-sune-id]');if(btn){const id=btn.getAttribute('data-sune-id');if(id){su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();closeSidebar()}return}const t=e.target.closest('[data-folder-toggle]');if(t){const id=t.getAttribute('data-folder-toggle');const f=folders.find(x=>x.id===id);if(f){f.open=!f.open;fo.save(folders);const block=document.querySelector(`.f-item[data-folder-id="${id}"] .suneGroup`);if(block)block.classList.toggle('hidden',!f.open);renderSidebar()}return}const menuBtn=e.target.closest('[data-folder-menu]');if(menuBtn){e.stopPropagation();showFolderMenu(menuBtn,menuBtn.getAttribute('data-folder-menu'))}})
|
||||
function hideFolderMenu(){el.folderMenu.classList.add('hidden');folderMenuId=null}
|
||||
let folderMenuId=null
|
||||
function showFolderMenu(btn,id){folderMenuId=id;const r=btn.getBoundingClientRect();el.folderMenu.style.top=(r.bottom+4)+'px';el.folderMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.folderMenu.classList.remove('hidden');if(window.lucide)lucide.createIcons()}
|
||||
document.addEventListener('click',e=>{if(!el.folderMenu.contains(e.target)&&!e.target.closest('[data-folder-menu]'))hideFolderMenu()})
|
||||
el.folderMenu.addEventListener('click',e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!folderMenuId)return;if(act==='delete'){const f=folders.find(x=>x.id===folderMenuId);if(!f)return;if(confirm('Delete this folder? Its sunes will move to root.')){sunes.filter(s=>s.folderId===f.id).forEach(s=>{s.folderId=null});folders=folders.filter(x=>x.id!==f.id);fo.save(folders);su.save(sunes);renderSidebar()}hideFolderMenu()})
|
||||
const 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.')}})
|
||||
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)}
|
||||
const 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.sunesExportOption.addEventListener('click',()=>{const payload={version:1,sunes,activeId:su.getActiveId(),foldersData};dl(`sunes-${ts()}.json`,payload);toggleUserMenu(false)})
|
||||
el.sunesExportOption.addEventListener('click',()=>{const payload={version:2,sunes,folders,activeId:su.getActiveId()};dl(`sunes-${ts()}.json`,payload);toggleUserMenu(false)})
|
||||
el.sunesImportOption.addEventListener('click',()=>{importMode='sunes';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.threadsDedupOption.addEventListener('click',async()=>{toggleUserMenu(false);const all=(await idb.all()).sort((a,b)=>b.updatedAt-a.updatedAt);const seen=new Set();let removed=0;for(const t of all){const key=JSON.stringify((t.messages||[]).map(m=>[m.role,m.content]));if(seen.has(key)){await idb.del(t.id);removed+=1;if(state.currentThreadId===t.id){state.currentThreadId=null;clearChat()}}else seen.add(key)}await renderHistory();alert(`${removed} duplicate${removed===1?'':'s'} removed.`)})
|
||||
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');sunes=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||{})}));su.save(sunes);su.setActiveId(data.activeId&&sunes.some(x=>x.id===data.activeId)?data.activeId:sunes[0]?.id||null);foldersData=data.foldersData&&data.foldersData.folders?data.foldersData:{folders:[],order:{}};if(!foldersData.order.root)foldersData.order.root=sunes.map(s=>s.id);fo.save(foldersData);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();alert('Sunes 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 th={id:gid(),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{alert('Import failed')}finally{importMode=null}})
|
||||
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');sunes=list.map(a=>({id:a.id||gid(),name:a.name||'Imported',order:a.order||0,folderId:a.folderId||null,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||{})}));folders=(Array.isArray(data.folders)?data.folders:[]).map(f=>({id:f.id||gid(),name:f.name||'Folder',open:f.open!==false,order:f.order||0}));su.save(sunes);fo.save(folders);su.setActiveId(data.activeId&&sunes.some(x=>x.id===data.activeId)?data.activeId:sunes[0]?.id||null);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();alert('Sunes 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 th={id:gid(),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{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)}))}
|
||||
function bindDND(){const setHint=(elr,pos)=>{document.querySelectorAll('.drop-hint').forEach(n=>n.classList.remove('drop-hint','drop-top','drop-bottom'));if(!elr)return;elr.classList.add('drop-hint');elr.classList.add(pos==='top'?'drop-top':'drop-bottom')}
|
||||
let dragId=null,dragSrcFolder=null
|
||||
const allRows=el.suneList.querySelectorAll('[data-row]'),containers=[el.suneList.querySelector('[data-root-list]'),...el.suneList.querySelectorAll('[data-folder-list]')]
|
||||
allRows.forEach(r=>{const btn=r.querySelector('[data-sune-id]');btn.addEventListener('dragstart',e=>{dragId=btn.getAttribute('data-sune-id');dragSrcFolder=(btn.closest('[data-folder-list]')?.getAttribute('data-folder-list'))||null;e.dataTransfer.effectAllowed='move';e.dataTransfer.setData('text/plain',dragId)});btn.addEventListener('dragend',()=>{setHint(null);dragId=null;dragSrcFolder=null});['touchstart'].forEach(ev=>btn.addEventListener(ev,()=>{btn.setAttribute('draggable','true')}));['touchend','touchcancel'].forEach(ev=>btn.addEventListener(ev,()=>{btn.setAttribute('draggable','true')}));r.addEventListener('dragover',e=>{if(!dragId)return;e.preventDefault();const rect=r.getBoundingClientRect(),pos=(e.clientY-rect.top)<rect.height/2?'top':'bottom';setHint(r,pos)});r.addEventListener('drop',e=>{if(!dragId)return;e.preventDefault();const targetId=r.querySelector('[data-sune-id]').getAttribute('data-sune-id');const tgtFolder=(r.closest('[data-folder-list]')?.getAttribute('data-folder-list'))||null;const arr=getOrder(tgtFolder);const fromArr=getOrder(dragSrcFolder);const fromIdx=fromArr.indexOf(dragId);if(fromIdx>-1){fromArr.splice(fromIdx,1);setOrder(dragSrcFolder,fromArr)}let idx=arr.indexOf(targetId);const top=r.classList.contains('drop-top')||false;idx=top?idx:idx+1;arr.splice(idx,0,dragId);setOrder(tgtFolder,arr);renderSidebar()})})
|
||||
containers.forEach(c=>{c.addEventListener('dragover',e=>{if(!dragId)return;e.preventDefault();setHint(null)});c.addEventListener('drop',e=>{if(!dragId)return;e.preventDefault();const fid=c.getAttribute('data-folder-list')||null;const arr=getOrder(fid);const fromArr=getOrder(dragSrcFolder);const fromIdx=fromArr.indexOf(dragId);if(fromIdx>-1){fromArr.splice(fromIdx,1);setOrder(dragSrcFolder,fromArr)}if(!arr.includes(dragId)){arr.push(dragId);setOrder(fid,arr)}renderSidebar()})})
|
||||
el.suneList.querySelectorAll('[data-folder-toggle]').forEach(b=>{b.addEventListener('dragover',e=>{if(!dragId)return;e.preventDefault()});b.addEventListener('drop',e=>{if(!dragId)return;e.preventDefault();const fid=b.getAttribute('data-folder-toggle');const arr=getOrder(fid);const fromArr=getOrder(dragSrcFolder);const fromIdx=fromArr.indexOf(dragId);if(fromIdx>-1){fromArr.splice(fromIdx,1);setOrder(dragSrcFolder,fromArr)}arr.unshift(dragId);setOrder(fid,arr);folderOpen.add(fid);fo.setOpen(folderOpen);renderSidebar()})})}
|
||||
function showFolderMenu(btn,id){const r=btn.getBoundingClientRect();el.folderMenu.style.top=(r.bottom+4)+'px';el.folderMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.folderMenu.dataset.fid=id;el.folderMenu.classList.remove('hidden');if(window.lucide)lucide.createIcons()}
|
||||
function hideFolderMenu(){el.folderMenu.classList.add('hidden');el.folderMenu.dataset.fid=''}
|
||||
el.folderMenu.addEventListener('click',e=>{const act=e.target.closest('[data-act]')?.getAttribute('data-act');const fid=el.folderMenu.dataset.fid;if(!act||!fid)return;if(act==='delete-folder'){const f=getFolder(fid);if(!f)return;if(confirm(`Delete folder "${f.name}"? Contents will move to root.`)){const arr=getOrder(fid);const root=getOrder('root');setOrder('root',[...root,...arr]);delete foldersData.order[fid];foldersData.folders=foldersData.folders.filter(x=>x.id!==fid);fo.save(foldersData);hideFolderMenu();renderSidebar()}}})
|
||||
async function init(){await idb.open();await renderHistory();renderSidebar();reflectActiveSune();clearChat();if(window.lucide)lucide.createIcons();fit();kbBind();kbUpdate()}
|
||||
async function init(){folders=fo.load().map(f=>Object.assign({open:true,order:0},f));sunes=su.load().map(a=>Object.assign({order:0,folderId:null},a));await idb.open();await renderHistory();renderSidebar();reflectActiveSune();clearChat();if(window.lucide)lucide.createIcons();fit();kbBind();kbUpdate()}
|
||||
window.addEventListener('resize',()=>{hideHistoryMenu();hideFolderMenu()})
|
||||
init()
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user