mirror of
https://github.com/multipleof4/devsune.git
synced 2026-01-14 00:17:56 +00:00
Update index.html
This commit is contained in:
29
2/index.html
29
2/index.html
@@ -18,6 +18,10 @@
|
||||
.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}
|
||||
.code-wrap{position:relative}
|
||||
.code-wrap textarea{position:absolute;inset:0;background:transparent;color:transparent;caret-color:#111;white-space:pre;overflow:auto;resize:none}
|
||||
.code-wrap pre{margin:0;height:100%;overflow:auto}
|
||||
.code-wrap textarea,.code-wrap pre{font:400 12px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;padding:.75rem;border-radius:.75rem;border:1px solid #e5e7eb}
|
||||
</style>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
</head>
|
||||
@@ -82,7 +86,7 @@
|
||||
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Sune Settings</span><button id="closeSettings" 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>
|
||||
<form id="settingsForm" class="text-sm">
|
||||
<div class="border-b flex text-xs font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">Script</button></div>
|
||||
<div class="border-b flex text-xs sm:text-sm font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">Script</button></div>
|
||||
<div id="panelModel" class="p-4 space-y-4">
|
||||
<div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="openai/gpt-4o"/></div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
@@ -100,8 +104,8 @@
|
||||
<div id="panelPrompt" class="p-4 space-y-4 hidden">
|
||||
<div><label class="block text-gray-700 font-medium mb-1">System Prompt</label><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-black/20" placeholder="Enter a system prompt to guide the sune"></textarea><p class="mt-1 text-xs text-gray-500">Saved per sune.</p></div>
|
||||
</div>
|
||||
<div id="panelScript" class="p-4 space-y-2 hidden">
|
||||
<div><label class="block text-gray-700 font-medium mb-1">Script</label><div class="grid gap-3 sm:grid-cols-2"><textarea id="set_script" class="w-full rounded-xl border border-gray-300 px-3 py-2 font-mono text-xs leading-5 h-[50vh] min-h-[50vh] resize-y" placeholder="// Write JS to customize this sune" spellcheck="false" autocorrect="off" autocomplete="off" autocapitalize="none" inputmode="text"></textarea><pre class="rounded-xl border border-gray-200 h-[50vh] min-h-[50vh] overflow-auto"><code id="scriptHL" class="language-javascript block px-3 py-2 text-xs leading-5"></code></pre></div><p class="mt-1 text-xs text-gray-500">Saved per sune. Preview updates as you type.</p></div>
|
||||
<div id="panelScript" class="p-4 space-y-3 hidden">
|
||||
<div><label class="block text-gray-700 font-medium mb-1">Script</label><div class="code-wrap h-72 sm:h-96"><pre class="rounded-xl"><code id="set_script_hl" class="language-javascript"></code></pre><textarea id="set_script" spellcheck="false" autocapitalize="none" autocomplete="off" autocorrect="off" class="w-full h-full" placeholder="Write custom JavaScript for this sune..."></textarea></div><p class="mt-1 text-xs text-gray-500">Syntax-highlighted as you type. Saved per sune and included in import/export.</p></div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
|
||||
<button type="button" id="deleteSuneBtn" class="inline-flex items-center gap-2 px-3 py-2 rounded-xl border border-red-200 text-red-700 hover:bg-red-50"><svg viewBox="0 0 24 24" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6v12a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V6m-9 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span>Delete sune</span></button>
|
||||
@@ -117,7 +121,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script>
|
||||
<script>
|
||||
const DEFAULT_MODEL='openai/gpt-5-chat',DEFAULT_API_KEY=''
|
||||
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript','panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_presence_penalty','set_repetition_penalty','set_min_p','set_top_a','set_reasoning_effort','set_system_prompt','set_script','scriptHL','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput'].map(id=>[id,document.getElementById(id)]))
|
||||
const el=Object.fromEntries(['chat','messages','composer','input','sendBtn','settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript','panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_presence_penalty','set_repetition_penalty','set_min_p','set_top_a','set_reasoning_effort','set_system_prompt','set_script','set_script_hl','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','apiKeyOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput'].map(id=>[id,document.getElementById(id)]))
|
||||
const icons=()=>window.lucide&&lucide.createIcons()
|
||||
const clamp=(v,min,max)=>Math.max(min,Math.min(max,v)),num=(v,d)=>v==null||v===''||isNaN(+v)?d:+v,int=(v,d)=>v==null||v===''||isNaN(parseInt(v))?d:parseInt(v),gid=()=>Math.random().toString(36).slice(2,9),esc=s=>String(s).replace(/[&<>'"`]/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'","`":"`"}[c]))
|
||||
const globalStore={get apiKey(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKey(v){localStorage.setItem('openrouter_api_key',v||'')}}
|
||||
@@ -169,19 +173,16 @@ return null}
|
||||
el.attachBtn.addEventListener('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.value=''};el.fileInput.click()})
|
||||
el.fileInput.addEventListener('change',async()=>{const files=[...(el.fileInput.files||[])];if(!files.length)return;for(const f of files){const part=await fileToPart(f).catch(()=>null);if(part)state.attachments.push(part)}updateAttachBadge()})
|
||||
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return;if(state.messages.length===0)state.currentThreadId=null;await ensureThreadOnFirstUser(text||'(attachments)');el.input.value='';const names=state.attachments.map(a=>a.label).join(', ');const display=text+(names?`\n\n**Attachments:** ${esc(names)}`:'');const contentParts=[];if(text)contentParts.push({type:'text',text});state.attachments.forEach(a=>contentParts.push(a.part));addMessage({role:'user',content:display,contentParts});state.busy=true;setBtnStop();const a=getActiveSune();const suneMeta={sune_name:a.name,model:store.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);let buf='',completed=false;await askOpenRouterStreaming((delta,done)=>{buf+=delta;renderMarkdown(suneBubble,buf,{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);state.messages.push({role:'assistant',content:buf,...suneMeta});persistThread();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}))}});state.attachments=[];updateAttachBadge()})
|
||||
function showTab(n){const on=(b,p)=>{b.classList.toggle('border-black',p);b.classList.toggle('border-transparent',!p)};on(el.tabModel,n==='model');on(el.tabPrompt,n==='prompt');on(el.tabScript,n==='script');el.panelModel.classList.toggle('hidden',n!=='model');el.panelPrompt.classList.toggle('hidden',n!=='prompt');el.panelScript.classList.toggle('hidden',n!=='script');if(n==='script')updateScriptHL()}
|
||||
function openSettings(){const a=getActiveSune(),s=a.settings;el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_presence_penalty.value=s.presence_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;el.set_script.value=s.script||'';showTab('model');el.settingsModal.classList.remove('hidden');updateScriptHL()}
|
||||
const showTab=name=>{[['panelModel','tabModel'],['panelPrompt','tabPrompt'],['panelScript','tabScript']].forEach(([p,b])=>{el[p].classList.toggle('hidden',p!=='panel'+name);el[b].classList.toggle('border-black',b==='tab'+name)})}
|
||||
function openSettings(){const a=getActiveSune(),s=a.settings;el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_presence_penalty.value=s.presence_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;el.set_script.value=s.script||'';updateScriptHL();showTab('Model');el.settingsModal.classList.remove('hidden')}
|
||||
const closeSettings=()=>{el.settingsModal.classList.add('hidden')}
|
||||
el.settingsBtnTop.addEventListener('click',openSettings)
|
||||
el.closeSettings.addEventListener('click',closeSettings)
|
||||
el.cancelSettings.addEventListener('click',closeSettings)
|
||||
el.settingsModal.addEventListener('click',e=>{if(e.target===el.settingsModal||e.target.classList.contains('bg-black/30'))closeSettings()})
|
||||
el.tabModel.addEventListener('click',()=>showTab('model'))
|
||||
el.tabPrompt.addEventListener('click',()=>showTab('prompt'))
|
||||
el.tabScript.addEventListener('click',()=>showTab('script'))
|
||||
function updateScriptHL(){const c=el.scriptHL;if(!c)return;const t=el.set_script.value||'';c.textContent=t;try{window.hljs&&hljs.highlightElement(c)}catch{}const pre=c.parentElement;pre.scrollTop=el.set_script.scrollTop;pre.scrollLeft=el.set_script.scrollLeft}
|
||||
el.set_script?.addEventListener('input',updateScriptHL)
|
||||
el.set_script?.addEventListener('scroll',()=>{const p=el.scriptHL.parentElement;p.scrollTop=el.set_script.scrollTop;p.scrollLeft=el.set_script.scrollLeft})
|
||||
el.tabModel.addEventListener('click',()=>showTab('Model'))
|
||||
el.tabPrompt.addEventListener('click',()=>showTab('Prompt'))
|
||||
el.tabScript.addEventListener('click',()=>showTab('Script'))
|
||||
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.reasoning_effort=(el.set_reasoning_effort.value||'default');s.system_prompt=el.set_system_prompt.value.trim();s.script=el.set_script.value; a.updatedAt=Date.now();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;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(),pinned:false,avatar:'',updatedAt:Date.now(),settings:Object.assign({},defaultSettings)});su.save(sunes);su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')})
|
||||
@@ -196,7 +197,9 @@ el.threadsImportOption.addEventListener('click',()=>{importMode='threads';el.imp
|
||||
el.importInput.addEventListener('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const text=await file.text();const data=JSON.parse(text);if(importMode==='sunes'){const list=Array.isArray(data)?data:(Array.isArray(data.sunes)?data.sunes:[]);if(!list.length)throw new Error('No sunes');const incoming=list.map(a=>makeSune(a||{}));const map={};incoming.forEach(s=>{if(!s.id)s.id=gid();const k=s.id,prev=map[k];map[k]=!prev||(+s.updatedAt>+prev.updatedAt)?s:prev});let added=0,updated=0;const idx=Object.fromEntries(sunes.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex){sunes.push(s);added++}else if(+s.updatedAt>+ex.updatedAt){Object.assign(ex,s);updated++}});su.save(sunes);if(data.activeId&&sunes.some(x=>x.id===data.activeId))su.setActiveId(data.activeId);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();alert(`${added} new, ${updated} updated.`)}else if(importMode==='threads'){const arr=Array.isArray(data)?data:(Array.isArray(data.threads)?data.threads:[]);if(!arr.length)throw new Error('No threads');const norm=t=>({id:t.id||gid(),title:titleFrom(t.title||titleFrom(t.messages?.find?.(m=>m.role==='user')?.content||'')),pinned:!!t.pinned,updatedAt:t.updatedAt||Date.now(),messages:Array.isArray(t.messages)?t.messages.filter(m=>m&&m.role&&m.content):[]});const best={};arr.forEach(t=>{const n=norm(t),k=n.id,prev=best[k];best[k]=!prev||(+n.updatedAt>+prev.updatedAt)?n:prev});let kept=0,skipped=0;for(const th of Object.values(best)){const ex=await idb.get(th.id);if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}await idb.put(th);kept++}await renderHistory();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
|
||||
function kbUpdate(){const vv=window.visualViewport;const overlap=vv?Math.max(0,(window.innerHeight-(vv.height+vv.offsetTop))):0;document.documentElement.style.setProperty('--kb',overlap+'px');const fh=el.footer.getBoundingClientRect().height;document.documentElement.style.setProperty('--footer-h',fh+'px');el.footer.style.transform='translateY('+(-overlap)+'px)';el.chat.style.scrollPaddingBottom=(fh+overlap+16)+'px'}
|
||||
function kbBind(){if(window.visualViewport){['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,()=>kbUpdate(),{passive:true}))}['resize','orientationchange'].forEach(ev=>window.addEventListener(ev,()=>setTimeout(kbUpdate,50),{passive:true}));['focus','click'].forEach(ev=>el.input.addEventListener(ev,()=>{setTimeout(()=>{kbUpdate();el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)}))}
|
||||
async function init(){await idb.open();await renderHistory();renderSidebar();reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
|
||||
function updateScriptHL(){el.set_script_hl.textContent=el.set_script.value||'';if(window.hljs)hljs.highlightElement(el.set_script_hl);el.set_script_hl.parentElement.scrollTop=el.set_script.scrollTop}
|
||||
function bindScriptEditor(){['input','keyup','change'].forEach(ev=>el.set_script.addEventListener(ev,updateScriptHL));el.set_script.addEventListener('scroll',()=>{el.set_script_hl.parentElement.scrollTop=el.set_script.scrollTop});const sync=()=>{const ta=el.set_script,pre=el.set_script_hl.parentElement;pre.style.height=ta.clientHeight+'px'};new ResizeObserver(sync).observe(el.set_script);updateScriptHL()}
|
||||
async function init(){await idb.open();await renderHistory();renderSidebar();reflectActiveSune();clearChat();icons();kbBind();kbUpdate();bindScriptEditor()}
|
||||
window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()})
|
||||
init()
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user