mirror of
https://github.com/multipleof4/sune.git
synced 2026-01-13 16:17:55 +00:00
Refactor: Replace json-stream-parser with oboe.js
This commit is contained in:
@@ -154,7 +154,7 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/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/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/json-stream-parser@0.0.2/index.min.js"></script>
|
<script src="https://unpkg.com/oboe@2.1.5/dist/oboe-browser.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded',()=>{
|
document.addEventListener('DOMContentLoaded',()=>{
|
||||||
const DEFAULT_MODEL='google/gemini-2.5-pro',DEFAULT_API_KEY=''
|
const DEFAULT_MODEL='google/gemini-2.5-pro',DEFAULT_API_KEY=''
|
||||||
@@ -255,7 +255,7 @@ $(el.sunesExportOption).on('click',()=>{dl(`sunes-${ts()}.sune`,{version:1,sunes
|
|||||||
$(el.sunesImportOption).on('click',()=>{importMode='sunes';el.importInput.value='';el.importInput.click()})
|
$(el.sunesImportOption).on('click',()=>{importMode='sunes';el.importInput.value='';el.importInput.click()})
|
||||||
$(el.threadsExportOption).on('click',()=>{dl(`threads-${ts()}.json`,{version:1,threads:THREAD.list});el.userMenu.classList.add('hidden')})
|
$(el.threadsExportOption).on('click',()=>{dl(`threads-${ts()}.json`,{version:1,threads:THREAD.list});el.userMenu.classList.add('hidden')})
|
||||||
$(el.threadsImportOption).on('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()})
|
$(el.threadsImportOption).on('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()})
|
||||||
$(el.importInput).on('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{if(importMode==='threads'&&window.JSONParser&&file.size>5*1024*1024){alert('Streaming large import...');let p=0;const b={},n=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 parser=new JSONParser();parser.onValue=({value,stack})=>{if(typeof value!=='object'||value===null||Array.isArray(value))return;if(!stack.length||!Array.isArray(stack[stack.length-1].value))return;const isRoot=stack.length===1,isThreads=stack.length===2&&stack[0].key==='threads';if(isRoot||isThreads){const v=n(value);const k=v.id,prev=b[k];b[k]=!prev||(+v.updatedAt>+prev.updatedAt)?v:v;p++}};const reader=file.stream().pipeThrough(new TextDecoderStream()).getReader();(async()=>{try{while(true){const{value,done}=await reader.read();if(done)break;parser.write(value)}if(p===0)throw new Error('No threads parsed.');let kept=0,skipped=0;const idx=Object.fromEntries(THREAD.list.map(t=>[t.id,t]));for(const th of Object.values(b)){const ex=idx[th.id];if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}if(!ex)THREAD.list.push(th);else Object.assign(ex,th);kept++}await THREAD.save();await renderThreads();alert(`${kept} imported, ${skipped} skipped (older).`);el.userMenu.classList.add('hidden')}catch(e){alert('Import failed. '+e.message)}finally{importMode=null}})();return}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++}});SUNE.save();if(data.activeId&&sunes.some(x=>x.id===data.activeId))SUNE.setActive(data.activeId);renderSidebar();await 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;const idx=Object.fromEntries(THREAD.list.map(t=>[t.id,t]));for(const th of Object.values(best)){const ex=idx[th.id];if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}if(!ex)THREAD.list.push(th);else Object.assign(ex,th);kept++}await THREAD.save();await renderThreads();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
|
$(el.importInput).on('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{if(importMode==='threads'&&window.oboe&&file.size>5*1024*1024){alert('Streaming large import...');let p=0;const b={},n=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 proc=v=>{if(typeof v!=='object'||v===null||Array.isArray(v))return;const t=n(v),k=t.id,prev=b[k];b[k]=!prev||(+t.updatedAt>+prev.updatedAt)?t:prev;p++};oboe(file.stream()).node('threads.*',proc).node('!.*',proc).done(async()=>{if(p===0){alert('Import failed. No threads parsed.');importMode=null;return}let kept=0,skipped=0;const idx=Object.fromEntries(THREAD.list.map(t=>[t.id,t]));for(const th of Object.values(b)){const ex=idx[th.id];if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}if(!ex)THREAD.list.push(th);else Object.assign(ex,th);kept++}await THREAD.save();await renderThreads();alert(`${kept} imported, ${skipped} skipped (older).`);el.userMenu.classList.add('hidden');importMode=null}).fail(e=>{alert('Import failed. '+(e.thrown?e.thrown.message:'Parse error'));importMode=null});return}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++}});SUNE.save();if(data.activeId&&sunes.some(x=>x.id===data.activeId))SUNE.setActive(data.activeId);renderSidebar();await 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;const idx=Object.fromEntries(THREAD.list.map(t=>[t.id,t]));for(const th of Object.values(best)){const ex=idx[th.id];if(ex&&+ex.updatedAt>=+th.updatedAt){skipped++;continue}if(!ex)THREAD.list.push(th);else Object.assign(ex,th);kept++}await THREAD.save();await renderThreads();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 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}))}$(window).on('resize orientationchange',()=>setTimeout(kbUpdate,50));$(el.input).on('focus click',()=>{setTimeout(()=>{kbUpdate();el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)})}
|
function kbBind(){if(window.visualViewport){['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,()=>kbUpdate(),{passive:true}))}$(window).on('resize orientationchange',()=>setTimeout(kbUpdate,50));$(el.input).on('focus click',()=>{setTimeout(()=>{kbUpdate();el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)})}
|
||||||
function activeMeta(){return {sune_name:SUNE.name,model:SUNE.model,avatar:SUNE.avatar}}
|
function activeMeta(){return {sune_name:SUNE.name,model:SUNE.model,avatar:SUNE.avatar}}
|
||||||
|
|||||||
Reference in New Issue
Block a user