mirror of
https://github.com/multipleof4/sune.git
synced 2026-01-13 16:17:55 +00:00
title instant
This commit is contained in:
@@ -186,9 +186,9 @@ let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)
|
||||
const TKEY='threads_v1',tload=()=>localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[]),tsave=v=>localforage.setItem(TKEY,v)
|
||||
const cacheStore=localforage.createInstance({name:'master_cache'});
|
||||
async function ensureThreadOnFirstUser(text){let needNew=!state.currentThreadId;if(state.messages.length===0)state.currentThreadId=null;if(state.currentThreadId&&!threads.some(x=>x.id===state.currentThreadId))needNew=true;if(!needNew)return;const id=gid(),now=Date.now(),th={id,title:'',pinned:false,updatedAt:now,messages:[]};state.currentThreadId=id;threads.unshift(th);await tsave(threads);await renderThreads()}
|
||||
const generateTitleWithAI=async messages=>{const model=SUNE.titleModel,apiKey=SUNE.apiKeyOR;if(!model||!apiKey||!messages?.length)return null;const sysPrompt='You are a title generator. Your response must be a single, concise line of text, no more than 60 characters. Do not use markdown, quotes, or any other formatting. Just the title.';const convo=messages.filter(m=>m.role==='user'||m.role==='assistant').map(m=>`[${m.role==='user'?'User':'Assistant'}]: ${partsToText(m.content)}`).join('\n\n');if(!convo)return null;try{const r=await fetch("https://openrouter.ai/api/v1/chat/completions",{method:'POST',headers:{'Authorization':`Bearer ${apiKey}`,'Content-Type':'application/json'},body:JSON.stringify({model:model.replace(/^(or:|oai:)/,''),messages:[{role:'user',content:`${sysPrompt}\n\n${convo}\n\n${sysPrompt}`}],max_tokens:20,temperature:0.2})});if(!r.ok)return null;const d=await r.json();return(d.choices?.[0]?.message?.content?.trim()||'').replace(/["']/g,'')||null}catch(e){console.error('AI title gen failed:',e);return null}}
|
||||
async function persistThread(full=true){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId);if(!th)return;th.messages=[...state.messages];if(full){th.updatedAt=Date.now();if(!th.title){const aiTitle=await generateTitleWithAI(th.messages);th.title=titleFrom(aiTitle||partsToText(th.messages.find(m=>m.role==='user')?.content)||'Untitled')}}await tsave(threads);if(full)await renderThreads()}
|
||||
const threadRow=t=>`<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?'📌 ':''}${esc(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>`
|
||||
const generateTitleWithAI=async messages=>{const model=SUNE.titleModel,apiKey=SUNE.apiKeyOR;if(!model||!apiKey||!messages?.length)return null;const sysPrompt='You are a thread title generator. Your response must be a single, concise line of text, no more than 60 characters. Do not use markdown, quotes, or any other formatting. Just the title.';const convo=messages.filter(m=>m.role==='user'||m.role==='assistant').map(m=>`[${m.role==='user'?'User':'Assistant'}]: ${partsToText(m.content)}`).join('\n\n');if(!convo)return null;try{const r=await fetch("https://openrouter.ai/api/v1/chat/completions",{method:'POST',headers:{'Authorization':`Bearer ${apiKey}`,'Content-Type':'application/json'},body:JSON.stringify({model:model.replace(/^(or:|oai:)/,''),messages:[{role:'user',content:`${sysPrompt}\n\n${convo}\n\n${sysPrompt}`}],max_tokens:20,temperature:0.2})});if(!r.ok)return null;const d=await r.json();return(d.choices?.[0]?.message?.content?.trim()||'').replace(/["']/g,'')||null}catch(e){console.error('AI title gen failed:',e);return null}}
|
||||
async function persistThread(full=true){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId);if(!th)return;th.messages=[...state.messages];if(full){th.updatedAt=Date.now()}await tsave(threads);if(full)await renderThreads()}
|
||||
const threadRow=t=>`<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?'📌 ':''}${esc(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 renderThreads(){const list=[...threads].sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt));el.threadList.innerHTML=list.map(threadRow).join('');icons()}
|
||||
let menuThreadId=null;const hideThreadPopover=()=>{el.threadPopover.classList.add('hidden');menuThreadId=null}
|
||||
function showThreadPopover(btn,id){menuThreadId=id;el.threadPopover.classList.remove('hidden');positionPopover(btn,el.threadPopover);icons()}
|
||||
@@ -205,7 +205,7 @@ function addAttachmentTree(role,arr){if(!arr?.length)return;const id=gid(),text=
|
||||
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 at=await toAttach(f).catch(()=>null);if(at)state.attachments.push(at)}updateAttachBadge()})
|
||||
el.messages.addEventListener('click',async e=>{const a=e.target.closest('a[href^="#dl-"]');if(!a)return; e.preventDefault();const m=a.getAttribute('href').match(/^#dl-([^-]+)-(\d+)$/);if(!m)return;const id=m[1],i=+m[2];const msg=state.messages.find(x=>x.id===id),meta=msg?.attachmentsMeta?.[i];if(!meta)return;let blob;if(meta.mode==='dataURL'){blob=await (await fetch(meta.data)).blob()}else{const bin=Uint8Array.from(atob(meta.data),c=>c.charCodeAt(0));blob=new Blob([bin],{type:meta.mime||'application/octet-stream'})}const url=URL.createObjectURL(blob),dl=document.createElement('a');dl.href=url;dl.download=meta.name||'download';document.body.appendChild(dl);dl.click();dl.remove();URL.revokeObjectURL(url)})
|
||||
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 parts=[];if(text)parts.push({type:'text',text});state.attachments.forEach(a=>parts.push(a.part));addMessage({role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]});if(state.attachments.length)addAttachmentTree('user',state.attachments);state.busy=true;setBtnStop();const a=SUNE.active;const suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''};const suneBubble=addSuneBubbleStreaming(suneMeta);const streamId=sid();suneBubble.dataset.mid=streamId;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);persistThread(false);state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done)=>{buf+=delta;state.stream.text=buf;renderMarkdown(suneBubble,buf,{enhance:false});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);persistThread(true);state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)persistThread(false)};await askOpenRouterStreaming(onDelta,streamId);state.attachments=[];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;await ensureThreadOnFirstUser(text||'(attachments)');const th=threads.find(x=>x.id===state.currentThreadId),shouldGenTitle=th&&!th.title;el.input.value='';const parts=[];if(text)parts.push({type:'text',text});state.attachments.forEach(a=>parts.push(a.part));addMessage({role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]});if(state.attachments.length)addAttachmentTree('user',state.attachments);if(shouldGenTitle){const aiTitle=await generateTitleWithAI(state.messages);th.title=titleFrom(aiTitle||partsToText(state.messages.find(m=>m.role==='user')?.content)||'Untitled');await tsave(threads);await renderThreads()}state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},suneBubble=addSuneBubbleStreaming(suneMeta),streamId=sid();suneBubble.dataset.mid=streamId;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);persistThread(false);state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done)=>{buf+=delta;state.stream.text=buf;renderMarkdown(suneBubble,buf,{enhance:false});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);persistThread(true);state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)persistThread(false)};await askOpenRouterStreaming(onDelta,streamId);state.attachments=[];updateAttachBadge()})
|
||||
let jars={html:null,extension:null};const ensureJars=async()=>{if(jars.html&&jars.extension)return jars;const mod=await import('https://medv.io/codejar/codejar.js'),CodeJar=mod.CodeJar||mod.default,hl=e=>e.innerHTML=hljs.highlight(e.textContent,{language:'xml'}).value;if(!jars.html)jars.html=CodeJar(el.htmlEditor,hl,{tab:' '});if(!jars.extension)jars.extension=CodeJar(el.extensionHtmlEditor,hl,{tab:' '});return jars}
|
||||
let openedHTML=false
|
||||
function openSettings(){const a=SUNE.active,s=a.settings;openedHTML=false;el.suneURL.value=a.url||'';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_max_tokens.value=s.max_tokens||'';el.set_verbosity.value=s.verbosity||'';el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;showTab('Model');el.suneModal.classList.remove('hidden')}
|
||||
|
||||
Reference in New Issue
Block a user