diff --git a/index.html b/index.html index da1b3b3..6beb7ef 100644 --- a/index.html +++ b/index.html @@ -185,8 +185,9 @@ function localDemoReply(){return 'Tip: open the sidebar → Account & Backup to let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled' 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:titleFrom(text),pinned:false,updatedAt:now,messages:[]};state.currentThreadId=id;threads.unshift(th);await tsave(threads);await renderThreads()} -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)th.title=titleFrom(partsToText(th.messages.find(m=>m.role==='user')?.content)||'Untitled')}await tsave(threads);if(full)await renderThreads()} +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 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();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=>`
` 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}