mirror of
https://github.com/multipleof4/sune.git
synced 2026-03-17 03:01:03 +00:00
Feat: Add LaTeX support via markdown-it-mathjax3
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {streamChat,HTTP_BASE} from './streaming.js';
|
||||
import mathjax3 from 'https://esm.sh/markdown-it-mathjax3';
|
||||
(()=>{let k,v=visualViewport;const f=()=>{removeEventListener('popstate',f),document.activeElement?.blur()};v.onresize=()=>{let o=v.height<innerHeight;o!=k&&((k=o)?(history.pushState({k:1},''),addEventListener('popstate',f)):(removeEventListener('popstate',f),history.state?.k&&history.back()))}})()
|
||||
const DEFAULT_MODEL='google/gemini-3-pro-preview',DEFAULT_API_KEY=''
|
||||
const el=window.el=Object.fromEntries(['topbar','chat','messages','composer','input','sendBtn','suneBtnTop','suneModal','suneURL','settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript','panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k','set_frequency_penalty','set_repetition_penalty','set_min_p','set_top_a','set_verbosity','set_reasoning_effort','set_system_prompt','set_hide_composer','set_include_thoughts','set_json_output','set_img_output','set_aspect_ratio','set_image_size','aspectRatioContainer','set_ignore_master_prompt','deleteSuneBtn','sidebarLeft','sidebarOverlayLeft','sidebarBtnLeft','suneList','newSuneBtn','userMenuBtn','userMenu','accountSettingsOption','sunesImportOption','sunesExportOption','threadsImportOption','importInput','sidebarBtnRight','sidebarRight','sidebarOverlayRight','threadList','closeThreads','threadPopover','sunePopover','footer','attachBtn','attachBadge','fileInput','htmlEditor','extensionHtmlEditor','jsonSchemaEditor','htmlTab_index','htmlTab_extension','suneHtml','accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','set_master_prompt','set_provider','set_api_key_or','set_api_key_oai','set_api_key_g','set_api_key_claude','set_api_key_cf','set_title_model','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','accountTabGeneral','accountTabAPI','accountPanelGeneral','accountPanelAPI','set_gh_token','gcpSAInput','gcpSAUploadBtn','importAccountSettings','exportAccountSettings','importAccountSettingsInput','accountTabUser','accountPanelUser','set_user_name','userAvatarPreview','setUserAvatarBtn','userAvatarInput','set_donor','threadRepoInput','threadBackBtn','threadFolderBtn','threadSyncBtn'].map(id=>[id,$('#'+id)[0]]))
|
||||
@@ -30,7 +31,7 @@ const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.title=`Setti
|
||||
const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===SUNE.id?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.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 renderSidebar=window.renderSidebar=()=>{const list=[...SUNE.list].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()}
|
||||
function enhanceCodeBlocks(root,doHL=true){$(root).find('pre>code').each((i,code)=>{if(code.textContent.length>200000)return;const $pre=$(code).parent().addClass('relative rounded-xl border border-gray-200');if(!$pre.find('.code-actions').length){const len=code.textContent.length,countText=len>=1e3?(len/1e3).toFixed(1)+'K':len;const $btn=$('<button class="bg-slate-900 text-white rounded-lg py-1 px-2 text-xs opacity-85">Copy</button>').on('click',async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(code.innerText);$btn.text('Copied');setTimeout(()=>$btn.text('Copy'),1200)}catch{}});const $container=$('<div class="code-actions absolute top-2 right-2 flex items-center gap-2"></div>');$container.append($(`<span class="text-xs text-gray-500">${countText} chars</span>`),$btn);$pre.append($container)}if(doHL&&window.hljs&&code.textContent.length<100000)hljs.highlightElement(code)})}
|
||||
const md=window.markdownit({html:false,linkify:true,typographer:true,breaks:true})
|
||||
const md=window.markdownit({html:false,linkify:true,typographer:true,breaks:true}).use(mathjax3)
|
||||
const getSuneLabel=m=>{const name=(m&&m.sune_name)||SUNE.name,modelShort=getModelShort(m&&m.model);return `${name} · ${modelShort}`}
|
||||
function _createMessageRow(m){const role=typeof m==='string'?m:(m&&m.role)||'assistant',meta=typeof m==='string'?{}:m||{},isUser=role==='user',$row=$('<div class="flex flex-col gap-2"></div>'),$head=$('<div class="flex items-center gap-2 px-4"></div>'),$avatar=$('<div></div>');const uAva=isUser?USER.avatar:meta.avatar;uAva?$avatar.attr('class','msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden').html(`<img src="${esc(uAva)}" class="h-full w-full object-cover">`):$avatar.attr('class',`${isUser?'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`).text(isUser?'👤':'✺');const $name=$('<div class="text-xs font-medium text-gray-500"></div>').text(isUser?USER.name:getSuneLabel(meta));const $deleteBtn=$('<button class="p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-red-500" title="Delete message"><i data-lucide="x" class="h-4 w-4"></i></button>').on('click',async e=>{e.stopPropagation();state.messages=state.messages.filter(msg=>msg.id!==m.id);$row.remove();await THREAD.persist()});const $copyBtn=$('<button class="ml-auto p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-gray-600" title="Copy message"><i data-lucide="copy" class="h-4 w-4"></i></button>').on('click',async function(e){e.stopPropagation();try{await navigator.clipboard.writeText(partsToText(m));$(this).html('<i data-lucide="check" class="h-4 w-4 text-green-500"></i>');icons();setTimeout(()=>{$(this).html('<i data-lucide="copy" class="h-4 w-4"></i>');icons()},1200)}catch{}});$head.append($avatar,$name,$copyBtn,$deleteBtn);const $bubble=$(`<div class="${(isUser?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full'}"></div>`);$row.append($head,$bubble);return $row}
|
||||
function msgRow(m){const $row=_createMessageRow(m);$(el.messages).append($row);queueMicrotask(()=>{el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'});icons()});return $row.find('.msg-bubble')[0]}
|
||||
@@ -124,7 +125,7 @@ const pullThreads=async()=>{const u=el.threadRepoInput.value.trim();if(!u.starts
|
||||
$(el.threadRepoInput).on('change',async()=>{const u=el.threadRepoInput.value.trim();localStorage.setItem('thread_repo_url',u);if(state.currentThreadId){state.currentThreadId=null;clearChat()}el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);if(u.startsWith('gh://'))await pullThreads();else{await THREAD.load();await renderThreads()}});
|
||||
$(el.threadBackBtn).on('click',()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const p=u.split('/');if(p.length>3){p.pop();el.threadRepoInput.value=p.join('/');el.threadRepoInput.dispatchEvent(new Event('change'))}});
|
||||
$(el.threadFolderBtn).on('click',async()=>{const n=prompt('Folder name:');if(!n)return;THREAD.list.unshift({id:n.trim(),title:n.trim(),type:'folder',updatedAt:Date.now()});await THREAD.save();await renderThreads()});
|
||||
$(el.threadSyncBtn).on('click',async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const mode=confirm('Sync Threads:\nOK = Upload (Push)\nCancel = Download (Pull)');const info=parseGhUrl(u);try{if(mode){const remoteItems=await ghApi(`${info.apiPath}?ref=${info.branch}`)||[],remoteMap={};remoteItems.forEach(i=>{const d=deserializeThreadName(i.name);if(d)remoteMap[d.id]={name:i.name,sha:i.sha}});const toRemove=[];for(const t of THREAD.list){if(t.status==='deleted'){if(remoteMap[t.id]){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Delete thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch});await localforage.removeItem('rem_t_'+t.id)}toRemove.push(t.id);continue}if(t.type!=='thread')continue;if(t.status==='modified'||t.status==='new'){const newName=serializeThreadName(t),msgs=await localforage.getItem('rem_t_'+t.id);if(remoteMap[t.id]&&remoteMap[t.id].name!==newName){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Rename thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch})}const ex=await ghApi(`${info.apiPath}/${newName}?ref=${info.branch}`);await ghApi(`${info.apiPath}/${newName}`,'PUT',{message:`Sync thread ${t.id}`,content:utob(JSON.stringify(msgs,null,2)),branch:info.branch,sha:ex?.sha});t.status='synced'}}THREAD.list=THREAD.list.filter(x=>!toRemove.includes(x.id));await THREAD.save();alert('Pushed to GitHub.')}else{await pullThreads();alert('Pulled from GitHub.')}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}});
|
||||
$(el.threadSyncBtn).on('click',async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const mode=confirm('Sync Threads:\nOK = Upload (Push)\nCancel = Download (Pull)');const info=parseGhUrl(u);try{if(mode){const remoteItems=await ghApi(`${info.apiPath}?ref=${info.branch}`)||[],remoteMap={};remoteItems.forEach(i=>{const d=deserializeThreadName(i.name);if(d)remoteMap[d.id]={name:i.name,sha:i.sha}});const toRemove=[];for(const t of THREAD.list){if(t.status==='deleted'){if(remoteMap[t.id]){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Delete thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch});await localforage.removeItem('rem_t_'+t.id)}toRemove.push(t.id);continue}if(t.type!=='thread')continue;if(t.status==='modified'||t.status==='new'){const newName=serializeThreadName(t),msgs=await localforage.getItem('rem_t_'+t.id);if(remoteMap[t.id]&&remoteMap[t.id].name!==newName){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Rename thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch})}const x=await ghApi(`${info.apiPath}/${newName}?ref=${info.branch}`);await ghApi(`${info.apiPath}/${newName}`,'PUT',{message:`Sync thread ${t.id}`,content:utob(JSON.stringify(msgs,null,2)),branch:info.branch,sha:x?.sha});t.status='synced'}}THREAD.list=THREAD.list.filter(x=>!toRemove.includes(x.id));await THREAD.save();alert('Pushed to GitHub.')}else{await pullThreads();alert('Pulled from GitHub.')}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}});
|
||||
init()
|
||||
const accountTabs={General:['accountTabGeneral','accountPanelGeneral'],API:['accountTabAPI','accountPanelAPI'],User:['accountTabUser','accountPanelUser']};function showAccountTab(key){Object.entries(accountTabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)})}
|
||||
function openAccountSettings(){el.set_provider.value=USER.provider||'openrouter';el.set_api_key_or.value=USER.apiKeyOpenRouter||'';el.set_api_key_oai.value=USER.apiKeyOpenAI||'';el.set_api_key_g.value=USER.apiKeyGoogle||'';el.set_api_key_claude.value=USER.apiKeyClaude||'';el.set_api_key_cf.value=USER.apiKeyCloudflare||'';el.set_master_prompt.value=USER.masterPrompt||'';el.set_title_model.value=USER.titleModel;el.set_gh_token.value=USER.githubToken||'';const sa=USER.gcpSA;el.gcpSAUploadBtn.textContent=sa&&sa.project_id?`Uploaded: ${sa.project_id}`:'Upload .json';el.set_user_name.value=USER.name;el.userAvatarPreview.src=USER.avatar||'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';el.userAvatarPreview.classList.toggle('bg-gray-200',!USER.avatar);el.set_donor.checked=USER.donor;const updateProv=()=>{const d=el.set_donor.checked;Array.from(el.set_provider.options).forEach(o=>{if(o.value!=='openrouter'){o.disabled=!d;if(!d)o.hidden=true;else o.hidden=false}});if(!d&&el.set_provider.value!=='openrouter')el.set_provider.value='openrouter'};updateProv();el.set_donor.onchange=updateProv;showAccountTab('General');el.accountSettingsModal.classList.remove('hidden')}
|
||||
@@ -152,6 +153,3 @@ const getActiveHtmlParts=()=>!el.htmlEditor.classList.contains('hidden')?[el.htm
|
||||
$(el.copyHTML).on('click',async()=>{try{await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent||'')}catch{}})
|
||||
$(el.pasteHTML).on('click',async()=>{try{const t=await navigator.clipboard.readText();const[editor,jar]=getActiveHtmlParts();if(jar&&jar.updateCode)jar.updateCode(t);else if(editor)editor.textContent=t}catch{}})
|
||||
Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,enhanceCodeBlocks,getSuneLabel,_createMessageRow,msgRow,partsToText,addSuneBubbleStreaming,clearChat,payloadWithSampling,setBtnStop,setBtnSend,localDemoReply,titleFrom,serializeThreadName,deserializeThreadName,ensureThreadOnFirstUser,generateTitleWithAI,threadRow,renderThreads,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,updateAttachBadge,toAttach,ensureJars,openSettings,closeSettings,showTab,dl,ts,kbUpdate,kbBind,activeMeta,init,showHtmlTab,showAccountTab,openAccountSettings,closeAccountSettings,getBubbleById,syncActiveThread,syncWhileBusy,onForeground,getActiveHtmlParts,imgToWebp,cacheStore,ghApi,parseGhUrl,pullThreads});
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user