This build was committed by a bot.
1
.gitignore
vendored
@@ -8,7 +8,6 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
|||||||
0
docs/.nojekyll → dist/.nojekyll
vendored
0
docs/CNAME → dist/CNAME
vendored
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 675 KiB After Width: | Height: | Size: 675 KiB |
6
docs/index.html → dist/index.html
vendored
@@ -231,7 +231,7 @@ window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()})
|
|||||||
init()
|
init()
|
||||||
const WS_BASE='wss://orp.awww.workers.dev/ws'
|
const WS_BASE='wss://orp.awww.workers.dev/ws'
|
||||||
const buildBody=()=>{const msgs=[];if(store.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:store.masterPrompt}]});if(store.system_prompt)msgs.push({role:'system',content:[{type:'text',text:store.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:store.model,messages:msgs,stream:true});if(store.reasoning_effort&&store.reasoning_effort!=='default')b.reasoning={effort:store.reasoning_effort};if(store.verbosity)b.verbosity=store.verbosity;return b}
|
const buildBody=()=>{const msgs=[];if(store.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:store.masterPrompt}]});if(store.system_prompt)msgs.push({role:'system',content:[{type:'text',text:store.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:store.model,messages:msgs,stream:true});if(store.reasoning_effort&&store.reasoning_effort!=='default')b.reasoning={effort:store.reasoning_effort};if(store.verbosity)b.verbosity=store.verbosity;return b}
|
||||||
async function askOpenRouterStreaming(onDelta,streamId){if(!store.apiKey){const t=localDemoReply();onDelta(t,true);return {ok:true,rid:streamId||null}}const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null};const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const ws=new WebSocket(WS_BASE+'?uid='+encodeURIComponent(r.rid));r.ws=ws;ws.onopen=()=>ws.send(JSON.stringify({type:'begin',rid:r.rid,provider:store.provider,apiKey:store.apiKey,or_body:buildBody()}));ws.onmessage=e=>{let m;try{m=JSON.parse(e.data)}catch{return}if(m.type==='delta'&&typeof m.seq==='number'&&m.seq>r.seq){r.seq=m.seq;onDelta(m.text||'',false)}else if(m.type==='done'){r.done=true;signal('');ws.close()}else if(m.type==='err'){r.done=true;signal('\n\n'+(m.message||'error'));ws.close()}};ws.onclose=()=>{};ws.onerror=()=>{};state.controller={abort:()=>{r.done=true;try{ws.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{}try{ws.close()}catch{}signal('')}};return {ok:true,rid:r.rid}}
|
async function askOpenRouterStreaming(onDelta,streamId){if(!store.apiKey){const t=localDemoReply();onDelta(t,true);return {ok:true,rid:streamId||null}}const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null};const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const ws=new WebSocket(WS_BASE+'?uid='+encodeURIComponent(r.rid));r.ws=ws;ws.onopen=()=>ws.send(JSON.stringify({type:'begin',rid:r.rid,provider:store.provider,apiKey:store.apiKey,or_body:buildBody()}));ws.onmessage=e=>{let m;try{m=JSON.parse(e.data)}catch{return}if(m.type==='delta'&&typeof m.seq==='number'&&m.seq>r.seq){r.seq=m.seq;onDelta(m.text||'',false)}else if(m.type==='done'){r.done=true;signal('');ws.close()}else if(m.type==='err'){r.done=true;signal('\n\n'+(m.message||'error'));ws.close()}};ws.onclose=()=>{};ws.onerror=()=>{};state.controller={abort:()=>{r.done=true;try{if(ws.readyState===1)ws.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{}signal('')}};return {ok:true,rid:r.rid}}
|
||||||
function openAccountSettings(){el.set_provider.value=store.provider||'openrouter';el.set_api_key_or.value=store.apiKeyOR||'';el.set_api_key_oai.value=store.apiKeyOAI||'';el.set_master_prompt.value=store.masterPrompt||'';el.accountSettingsModal.classList.remove('hidden')}
|
function openAccountSettings(){el.set_provider.value=store.provider||'openrouter';el.set_api_key_or.value=store.apiKeyOR||'';el.set_api_key_oai.value=store.apiKeyOAI||'';el.set_master_prompt.value=store.masterPrompt||'';el.accountSettingsModal.classList.remove('hidden')}
|
||||||
function closeAccountSettings(){el.accountSettingsModal.classList.add('hidden')}
|
function closeAccountSettings(){el.accountSettingsModal.classList.add('hidden')}
|
||||||
el.accountSettingsOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()})
|
el.accountSettingsOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()})
|
||||||
@@ -245,8 +245,8 @@ const lastAssistantId=()=>{const a=[...el.messages.querySelectorAll('.msg-bubble
|
|||||||
const getBubbleById=id=>el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`)
|
const getBubbleById=id=>el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`)
|
||||||
async function syncTranscript(){const id=lastAssistantId();if(!id)return false;const bubble=getBubbleById(id);if(!bubble)return false;const j=await fetchTranscript(id).catch(()=>null);if(!j||j.rid!==id){if(j&&j.error){setBtnSend();state.busy=false;const t=(bubble.textContent||'')+'\n\n'+j.error;renderMarkdown(bubble,t,{enhance:false});enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread()}return false}const t=j.text||'';renderMarkdown(bubble,t,{enhance:false});if(j.error||j.done||j.phase==='done'){setBtnSend();state.busy=false;enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread();return false}return true}
|
async function syncTranscript(){const id=lastAssistantId();if(!id)return false;const bubble=getBubbleById(id);if(!bubble)return false;const j=await fetchTranscript(id).catch(()=>null);if(!j||j.rid!==id){if(j&&j.error){setBtnSend();state.busy=false;const t=(bubble.textContent||'')+'\n\n'+j.error;renderMarkdown(bubble,t,{enhance:false});enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread()}return false}const t=j.text||'';renderMarkdown(bubble,t,{enhance:false});if(j.error||j.done||j.phase==='done'){setBtnSend();state.busy=false;enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread();return false}return true}
|
||||||
let syncLoopRunning=false
|
let syncLoopRunning=false
|
||||||
const syncKick=r=>{if(document.visibilityState!=='hidden')setTimeout(()=>syncLoop(r),0)}
|
const syncKick=r=>{if(!state.busy||document.visibilityState==='hidden')return;setTimeout(()=>syncLoop(r),0)}
|
||||||
async function syncLoop(){if(syncLoopRunning)return;const id=lastAssistantId();if(!id)return;syncLoopRunning=true;try{for(;;){const cont=await syncTranscript().catch(()=>false);if(!cont)break;await new Promise(r=>setTimeout(r,1200))}}finally{syncLoopRunning=false}}
|
async function syncLoop(){if(!state.busy||syncLoopRunning)return;const id=lastAssistantId();if(!id)return;syncLoopRunning=true;try{for(;;){if(!state.busy)break;const cont=await syncTranscript().catch(()=>false);if(!cont)break;await new Promise(r=>setTimeout(r,1200))}}finally{syncLoopRunning=false}}
|
||||||
;['focus','pageshow'].forEach(ev=>window.addEventListener(ev,()=>syncKick(ev)))
|
;['focus','pageshow'].forEach(ev=>window.addEventListener(ev,()=>syncKick(ev)))
|
||||||
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')syncKick('visible')})
|
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')syncKick('visible')})
|
||||||
el.copySystemPrompt.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}})
|
el.copySystemPrompt.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}})
|
||||||
0
docs/registerSW.js → dist/registerSW.js
vendored
2
docs/sw.js → dist/sw.js
vendored
@@ -1 +1 @@
|
|||||||
if(!self.define){let e,i={};const t=(t,n)=>(t=new URL(t+".js",n).href,i[t]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=t,e.onload=i,document.head.appendChild(e)}else e=t,importScripts(t),i()}).then(()=>{let e=i[t];if(!e)throw new Error(`Module ${t} didn’t register its module`);return e}));self.define=(n,r)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let o={};const c=e=>t(e,s),d={module:{uri:s},exports:o,require:c};i[s]=Promise.all(n.map(e=>d[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-5ffe50d4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"806ca7b504dca47012ba05e0c3ddfc58"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
if(!self.define){let e,i={};const t=(t,n)=>(t=new URL(t+".js",n).href,i[t]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=t,e.onload=i,document.head.appendChild(e)}else e=t,importScripts(t),i()}).then(()=>{let e=i[t];if(!e)throw new Error(`Module ${t} didn’t register its module`);return e}));self.define=(n,r)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let o={};const d=e=>t(e,s),c={module:{uri:s},exports:o,require:d};i[s]=Promise.all(n.map(e=>c[e]||d(e))).then(e=>(r(...e),o))}}define(["./workbox-5ffe50d4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"6d99841722e382c2daa63eee681189df"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
||||||
0
docs/✺.avif → dist/✺.avif
vendored
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
100
index.html
@@ -16,7 +16,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white text-gray-900 selection:bg-black/10">
|
<body class="bg-white text-gray-900 selection:bg-black/10" hx-on="click: if(!document.getElementById('historyMenu').contains(event.target)&&!event.target.closest('[data-thread-menu]')) hideHistoryMenu(); if(!document.getElementById('suneMenu').contains(event.target)&&!event.target.closest('[data-sune-menu]')) hideSuneMenu(); if(!document.getElementById('userMenu').contains(event.target)&&!document.getElementById('userMenuBtn').contains(event.target)) document.getElementById('userMenu').classList.add('hidden')">
|
||||||
<div class="flex flex-col h-dvh max-h-dvh">
|
<div class="flex flex-col h-dvh max-h-dvh">
|
||||||
<header class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
|
<header class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
|
||||||
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
|
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
|
||||||
@@ -122,16 +122,7 @@
|
|||||||
<div><label class="block text-gray-700 font-medium mb-1">OpenRouter API Key</label><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-or-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
|
<div><label class="block text-gray-700 font-medium mb-1">OpenRouter API Key</label><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-or-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">OpenAI API Key</label><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
|
<div><label class="block text-gray-700 font-medium mb-1">OpenAI API Key</label><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
|
||||||
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
|
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
|
||||||
<div class="flex items-center justify-between pt-2 border-t">
|
<div class="flex items-center justify-end gap-2 pt-2 border-t"><button type="button" id="cancelAccountSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<button type="button" id="accountImportOption" class="text-xs text-gray-600 hover:underline text-left">Import settings</button>
|
|
||||||
<button type="button" id="accountExportOption" class="text-xs text-gray-600 hover:underline text-left">Export settings</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button type="button" id="cancelAccountSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button>
|
|
||||||
<button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,27 +132,14 @@
|
|||||||
<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>
|
<script>
|
||||||
const DEFAULT_MODEL='openai/gpt-4o',DEFAULT_API_KEY=''
|
const DEFAULT_MODEL='openai/gpt-5',DEFAULT_API_KEY=''
|
||||||
const el=Object.fromEntries([
|
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_max_tokens','set_verbosity','set_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','userMenuBtn','userMenu','accountSettingsOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu','suneMenu','footer','attachBtn','attachBadge','fileInput','scriptEditor','htmlEditor','subTabHTML','subTabJS','panelHTML','panelJS','suneHtml','accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','set_master_prompt','set_provider','set_api_key_or','set_api_key_oai','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','copyJS','pasteJS'].map(id=>[id,document.getElementById(id)]))
|
||||||
'chat','messages','composer','input','sendBtn','footer','attachBtn','attachBadge','fileInput',
|
|
||||||
'sidebar','sidebarOverlay','sidebarBtn','suneList','newSuneBtn','suneMenu','suneHtml',
|
|
||||||
'historyBtn','historyPanel','historyOverlay','historyList','closeHistory','historyMenu',
|
|
||||||
'userMenuBtn','userMenu','accountSettingsOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput',
|
|
||||||
'settingsBtnTop','settingsModal','settingsForm','closeSettings','cancelSettings','deleteSuneBtn',
|
|
||||||
'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_max_tokens','set_verbosity','set_reasoning_effort','set_system_prompt',
|
|
||||||
'copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','copyJS','pasteJS',
|
|
||||||
'subTabHTML','subTabJS','panelHTML','panelJS','htmlEditor','scriptEditor',
|
|
||||||
'accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','accountImportOption','accountExportOption',
|
|
||||||
'set_provider','set_api_key_or','set_api_key_oai','set_master_prompt'
|
|
||||||
].map(id=>[id,document.getElementById(id)]))
|
|
||||||
const icons=()=>window.lucide&&lucide.createIcons()
|
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 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 sid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6)
|
const sid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6)
|
||||||
const fmtSize=b=>{const u=['B','KB','MB','GB','TB'];let i=0,x=b;while(x>=1024&&i<u.length-1){x/=1024;i++}return (x>=10?Math.round(x):Math.round(x*10)/10)+' '+u[i]}
|
const fmtSize=b=>{const u=['B','KB','MB','GB','TB'];let i=0,x=b;while(x>=1024&&i<u.length-1){x/=1024;i++}return (x>=10?Math.round(x):Math.round(x*10)/10)+' '+u[i]}
|
||||||
const asDataURL=f=>new Promise(r=>{const fr=new FileReader();fr.onload=()=>r(String(fr.result||''));fr.readAsDataURL(f)})
|
const asDataURL=f=>new Promise(r=>{const fr=new FileReader();fr.onload=()=>r(String(fr.result||''));fr.readAsDataURL(f)})
|
||||||
const b64=x=>x.split(',')[1]||''
|
const b64=x=>x.split(',')[1]||''
|
||||||
const getUserId=()=>{let u=localStorage.getItem('sune_user_id');if(!u){u='user_'+Math.random().toString(36).slice(2,15);localStorage.setItem('sune_user_id',u)}return u}
|
|
||||||
const globalStore={get provider(){return localStorage.getItem('provider')||'openrouter'},set provider(v){localStorage.setItem('provider',v==='openai'?'openai':'openrouter')},get apiKeyOR(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKeyOR(v){localStorage.setItem('openrouter_api_key',v||'')},get apiKeyOAI(){return localStorage.getItem('openai_api_key')||''},set apiKeyOAI(v){localStorage.setItem('openai_api_key',v||'')},get masterPrompt(){return localStorage.getItem('master_prompt')||''},set masterPrompt(v){localStorage.setItem('master_prompt',v||'')}}
|
const globalStore={get provider(){return localStorage.getItem('provider')||'openrouter'},set provider(v){localStorage.setItem('provider',v==='openai'?'openai':'openrouter')},get apiKeyOR(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKeyOR(v){localStorage.setItem('openrouter_api_key',v||'')},get apiKeyOAI(){return localStorage.getItem('openai_api_key')||''},set apiKeyOAI(v){localStorage.setItem('openai_api_key',v||'')},get masterPrompt(){return localStorage.getItem('master_prompt')||''},set masterPrompt(v){localStorage.setItem('master_prompt',v||'')}}
|
||||||
const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}}
|
const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}}
|
||||||
const defaultSettings={model:DEFAULT_MODEL,temperature:1,top_p:0.97,top_k:0,frequency_penalty:0,presence_penalty:0,repetition_penalty:1,min_p:0,top_a:0,max_tokens:0,verbosity:'',reasoning_effort:'default',system_prompt:'',script:'',html:''}
|
const defaultSettings={model:DEFAULT_MODEL,temperature:1,top_p:0.97,top_k:0,frequency_penalty:0,presence_penalty:0,repetition_penalty:1,min_p:0,top_a:0,max_tokens:0,verbosity:'',reasoning_effort:'default',system_prompt:'',script:'',html:''}
|
||||||
@@ -195,12 +173,10 @@ async function ensureThreadOnFirstUser(text){let needNew=!state.currentThreadId;
|
|||||||
async function persistThread(){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId);if(!th)return;th.messages=[...state.messages];th.updatedAt=Date.now();th.title=titleFrom(partsToText(th.messages.find(m=>m.role==='user')?.content)||th.title);await tsave(threads);await renderHistory()}
|
async function persistThread(){if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId);if(!th)return;th.messages=[...state.messages];th.updatedAt=Date.now();th.title=titleFrom(partsToText(th.messages.find(m=>m.role==='user')?.content)||th.title);await tsave(threads);await renderHistory()}
|
||||||
const historyRow=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 historyRow=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 renderHistory(){const list=[...threads].sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt));el.historyList.innerHTML=list.map(historyRow).join('');icons()}
|
async function renderHistory(){const list=[...threads].sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt));el.historyList.innerHTML=list.map(historyRow).join('');icons()}
|
||||||
let menuThreadId=null,menuSuneId=null
|
let menuThreadId=null;const hideHistoryMenu=()=>{el.historyMenu.classList.add('hidden');menuThreadId=null}
|
||||||
const hideHistoryMenu=()=>{el.historyMenu.classList.add('hidden');menuThreadId=null}
|
function showHistoryMenu(btn,id){menuThreadId=id;const r=btn.getBoundingClientRect();el.historyMenu.style.top=(r.bottom+4)+'px';el.historyMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.historyMenu.classList.remove('hidden');icons()}
|
||||||
const hideSuneMenu=()=>{el.suneMenu.classList.add('hidden');menuSuneId=null}
|
let menuSuneId=null;const hideSuneMenu=()=>{el.suneMenu.classList.add('hidden');menuSuneId=null}
|
||||||
const showMenu=(menuEl,btn,setMenuId,id)=>{setMenuId(id);const r=btn.getBoundingClientRect();menuEl.style.top=`${r.bottom+4}px`;menuEl.style.left=`${Math.min(window.innerWidth-220,r.right-200)}px`;menuEl.classList.remove('hidden');icons()}
|
function showSuneMenu(btn,id){menuSuneId=id;const r=btn.getBoundingClientRect();el.suneMenu.style.top=(r.bottom+4)+'px';el.suneMenu.style.left=Math.min(window.innerWidth-220,r.right-200)+'px';el.suneMenu.classList.remove('hidden');icons()}
|
||||||
const showHistoryMenu=(btn,id)=>showMenu(el.historyMenu,btn,val=>menuThreadId=val,id)
|
|
||||||
const showSuneMenu=(btn,id)=>showMenu(el.suneMenu,btn,val=>menuSuneId=val,id)
|
|
||||||
el.historyList.addEventListener('click',async e=>{const openBtn=e.target.closest('[data-open-thread]'),menuBtn=e.target.closest('[data-thread-menu]');if(openBtn){const id=openBtn.getAttribute('data-open-thread'),th=threads.find(t=>t.id===id);if(!th)return;state.currentThreadId=id;renderSuneHTML();clearChat();state.messages=Array.isArray(th.messages)?[...th.messages]:[];for(const m of state.messages){const b=msgRow(m);b.dataset.mid=m.id||'';renderMarkdown(b,partsToText(m.content))}queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden');hideHistoryMenu();return}if(menuBtn){e.stopPropagation();showHistoryMenu(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}})
|
el.historyList.addEventListener('click',async e=>{const openBtn=e.target.closest('[data-open-thread]'),menuBtn=e.target.closest('[data-thread-menu]');if(openBtn){const id=openBtn.getAttribute('data-open-thread'),th=threads.find(t=>t.id===id);if(!th)return;state.currentThreadId=id;renderSuneHTML();clearChat();state.messages=Array.isArray(th.messages)?[...th.messages]:[];for(const m of state.messages){const b=msgRow(m);b.dataset.mid=m.id||'';renderMarkdown(b,partsToText(m.content))}queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.historyPanel.classList.add('translate-x-full');el.historyOverlay.classList.add('hidden');hideHistoryMenu();return}if(menuBtn){e.stopPropagation();showHistoryMenu(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}})
|
||||||
el.historyMenu.addEventListener('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=threads.find(t=>t.id===menuThreadId);if(!th)return;if(act==='pin'){th.pinned=!th.pinned}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now()}}else if(act==='delete'){if(confirm('Delete this chat?')){threads=threads.filter(x=>x.id!==th.id);if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=Array.isArray(th.messages)?th.messages:[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m.content||'')||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}hideHistoryMenu();await tsave(threads);renderHistory()})
|
el.historyMenu.addEventListener('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=threads.find(t=>t.id===menuThreadId);if(!th)return;if(act==='pin'){th.pinned=!th.pinned}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now()}}else if(act==='delete'){if(confirm('Delete this chat?')){threads=threads.filter(x=>x.id!==th.id);if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=Array.isArray(th.messages)?th.messages:[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m.content||'')||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}hideHistoryMenu();await tsave(threads);renderHistory()})
|
||||||
el.suneList.addEventListener('click',e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSuneMenu(menuBtn,menuBtn.getAttribute('[data-sune-menu]')?menuBtn.getAttribute('[data-sune-menu]'):menuBtn.getAttribute('data-sune-menu'));return}const btn=e.target.closest('[data-sune-id]');if(!btn)return;const id=btn.getAttribute('data-sune-id');if(id){su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')}})
|
el.suneList.addEventListener('click',e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSuneMenu(menuBtn,menuBtn.getAttribute('[data-sune-menu]')?menuBtn.getAttribute('[data-sune-menu]'):menuBtn.getAttribute('data-sune-menu'));return}const btn=e.target.closest('[data-sune-id]');if(!btn)return;const id=btn.getAttribute('data-sune-id');if(id){su.setActiveId(id);renderSidebar();reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebar').classList.add('-translate-x-full');document.getElementById('sidebarOverlay').classList.add('hidden')}})
|
||||||
@@ -216,17 +192,21 @@ el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy
|
|||||||
let jars={js:null,html:null};const ensureJars=async()=>{if(jars.js&&jars.html)return jars;const mod=await import('https://medv.io/codejar/codejar.js');const CodeJar=mod.CodeJar||mod.default;const mk=(elx,lang)=>CodeJar(elx,ed=>{ed.innerHTML=hljs.highlight(ed.textContent,{language:lang}).value},{tab:' '});if(!jars.js)jars.js=mk(el.scriptEditor,'javascript');if(!jars.html)jars.html=mk(el.htmlEditor,'xml');return jars}
|
let jars={js:null,html:null};const ensureJars=async()=>{if(jars.js&&jars.html)return jars;const mod=await import('https://medv.io/codejar/codejar.js');const CodeJar=mod.CodeJar||mod.default;const mk=(elx,lang)=>CodeJar(elx,ed=>{ed.innerHTML=hljs.highlight(ed.textContent,{language:lang}).value},{tab:' '});if(!jars.js)jars.js=mk(el.scriptEditor,'javascript');if(!jars.html)jars.html=mk(el.htmlEditor,'xml');return jars}
|
||||||
let openedJS=false,openedHTML=false
|
let openedJS=false,openedHTML=false
|
||||||
function openSettings(){const a=getActiveSune(),s=a.settings;openedJS=false;openedHTML=false;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.settingsModal.classList.remove('hidden')}
|
function openSettings(){const a=getActiveSune(),s=a.settings;openedJS=false;openedHTML=false;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.settingsModal.classList.remove('hidden')}
|
||||||
|
const closeSettings=()=>{el.settingsModal.classList.add('hidden')}
|
||||||
const tabs={Model:['tabModel','panelModel'],Prompt:['tabPrompt','panelPrompt'],Script:['tabScript','panelScript']}
|
const tabs={Model:['tabModel','panelModel'],Prompt:['tabPrompt','panelPrompt'],Script:['tabScript','panelScript']}
|
||||||
function showScriptSubTab(key){['HTML','JS'].forEach(k=>{const on=k===key;el['subTab'+k].classList.toggle('bg-black',on);el['subTab'+k].classList.toggle('text-white',on);el['subTab'+k].classList.toggle('bg-gray-100',!on);el['panel'+(k==='HTML'?'HTML':'JS')].classList.toggle('hidden',!on)});if(key==='HTML')openedHTML=true;else openedJS=true}
|
function showScriptSubTab(key){['HTML','JS'].forEach(k=>{const on=k===key;el['subTab'+k].classList.toggle('bg-black',on);el['subTab'+k].classList.toggle('text-white',on);el['subTab'+k].classList.toggle('bg-gray-100',!on);el['panel'+(k==='HTML'?'HTML':'JS')].classList.toggle('hidden',!on)});if(key==='HTML')openedHTML=true;else openedJS=true}
|
||||||
function showTab(key){Object.entries(tabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)});if(key==='Script'){ensureJars().then(({js,html})=>{const s=getActiveSune().settings;js.updateCode(s.script||'');html.updateCode(s.html||'')});showScriptSubTab('HTML')}}
|
function showTab(key){Object.entries(tabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)});if(key==='Script'){ensureJars().then(({js,html})=>{const s=getActiveSune().settings;js.updateCode(s.script||'');html.updateCode(s.html||'')});showScriptSubTab('HTML')}}
|
||||||
el.settingsBtnTop.addEventListener('click',openSettings)
|
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.tabModel.addEventListener('click',()=>showTab('Model'))
|
||||||
el.tabPrompt.addEventListener('click',()=>showTab('Prompt'))
|
el.tabPrompt.addEventListener('click',()=>showTab('Prompt'))
|
||||||
el.tabScript.addEventListener('click',()=>showTab('Script'))
|
el.tabScript.addEventListener('click',()=>showTab('Script'))
|
||||||
el.subTabHTML.addEventListener('click',()=>showScriptSubTab('HTML'))
|
el.subTabHTML.addEventListener('click',()=>showScriptSubTab('HTML'))
|
||||||
el.subTabJS.addEventListener('click',()=>showScriptSubTab('JS'))
|
el.subTabJS.addEventListener('click',()=>showScriptSubTab('JS'))
|
||||||
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.max_tokens=Math.max(0,int(el.set_max_tokens.value,0));s.verbosity=(el.set_verbosity.value||'');s.reasoning_effort=(el.set_reasoning_effort.value||'default');s.system_prompt=el.set_system_prompt.value.trim();const oldScript=s.script,oldHtml=s.html;const edScript=el.scriptEditor?.textContent||'',edHtml=el.htmlEditor?.textContent||'';s.script=openedJS?edScript:oldScript;s.html=openedHTML?edHtml:oldHtml;a.updatedAt=Date.now();su.save(sunes);el.settingsModal.classList.add('hidden');reflectActiveSune()})
|
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.max_tokens=Math.max(0,int(el.set_max_tokens.value,0));s.verbosity=(el.set_verbosity.value||'');s.reasoning_effort=(el.set_reasoning_effort.value||'default');s.system_prompt=el.set_system_prompt.value.trim();const oldScript=s.script,oldHtml=s.html;const edScript=el.scriptEditor?.textContent||'',edHtml=el.htmlEditor?.textContent||'';s.script=openedJS?edScript:oldScript;s.html=openedHTML?edHtml:oldHtml;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();el.settingsModal.classList.add('hidden')})
|
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')})
|
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')})
|
||||||
function dl(name,obj){const blob=new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
|
function dl(name,obj){const blob=new Blob([JSON.stringify(obj,null,2)],{type:'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
|
||||||
const ts=()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`}
|
const ts=()=>{const d=new Date(),p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`}
|
||||||
@@ -235,51 +215,41 @@ el.sunesExportOption.addEventListener('click',()=>{dl(`sunes-${ts()}.json`,{vers
|
|||||||
el.sunesImportOption.addEventListener('click',()=>{importMode='sunes';el.importInput.value='';el.importInput.click()})
|
el.sunesImportOption.addEventListener('click',()=>{importMode='sunes';el.importInput.value='';el.importInput.click()})
|
||||||
el.threadsExportOption.addEventListener('click',()=>{dl(`threads-${ts()}.json`,{version:1,threads});el.userMenu.classList.add('hidden')})
|
el.threadsExportOption.addEventListener('click',()=>{dl(`threads-${ts()}.json`,{version:1,threads});el.userMenu.classList.add('hidden')})
|
||||||
el.threadsImportOption.addEventListener('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()})
|
el.threadsImportOption.addEventListener('click',()=>{importMode='threads';el.importInput.value='';el.importInput.click()})
|
||||||
el.accountExportOption.addEventListener('click',()=>{dl(`account-settings-${ts()}.json`,{provider:store.provider,apiKeyOR:store.apiKeyOR,apiKeyOAI:store.apiKeyOAI,masterPrompt:store.masterPrompt})})
|
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;const idx=Object.fromEntries(threads.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)threads.push(th);else Object.assign(ex,th);kept++}await tsave(threads);await renderHistory();alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}})
|
||||||
el.accountImportOption.addEventListener('click',()=>{importMode='account';el.importInput.value='';el.importInput.click()})
|
|
||||||
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.`);el.userMenu.classList.add('hidden')}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(threads.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)threads.push(th);else Object.assign(ex,th);kept++}await tsave(threads);await renderHistory();alert(`${kept} imported, ${skipped} skipped (older).`);el.userMenu.classList.add('hidden')}else if(importMode==='account'){const{provider,apiKeyOR,apiKeyOAI,masterPrompt}=data;if(provider)store.provider=provider;if(apiKeyOR)store.apiKeyOR=apiKeyOR;if(apiKeyOAI)store.apiKeyOAI=apiKeyOAI;if(masterPrompt)store.masterPrompt=masterPrompt;el.set_provider.value=store.provider;el.set_api_key_or.value=store.apiKeyOR;el.set_api_key_oai.value=store.apiKeyOAI;el.set_master_prompt.value=store.masterPrompt;alert('Account settings imported.')}}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}))}['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)}))}
|
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)}))}
|
||||||
function activeMeta(){const a=getActiveSune();return {sune_name:a.name,model:store.model,avatar:a.avatar||''}}
|
function activeMeta(){const a=getActiveSune();return {sune_name:a.name,model:store.model,avatar:a.avatar||''}}
|
||||||
window.SUNE={attach:async(files,opts={})=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser(clean[0]?.name||'(attachments)');const meta=activeMeta();const o=typeof opts==='boolean'?{toAPI:opts,tree:true}:(opts||{});const toAPI=('toAPI'in o)?!!o.toAPI:(('toapi'in o)?!!o.toapi:true);const tree=('tree'in o)?!!o.tree:true;if(toAPI){const parts=clean.map(a=>a.part);addMessage({role:'assistant',content:parts,...meta})}if(tree)addAttachmentTree('assistant',clean);await persistThread()},log:async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await persistThread()}}
|
window.SUNE={attach:async(files,opts={})=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser(clean[0]?.name||'(attachments)');const meta=activeMeta();const o=typeof opts==='boolean'?{toAPI:opts,tree:true}:(opts||{});const toAPI=('toAPI'in o)?!!o.toAPI:(('toapi'in o)?!!o.toapi:true);const tree=('tree'in o)?!!o.tree:true;if(toAPI){const parts=clean.map(a=>a.part);addMessage({role:'assistant',content:parts,...meta})}if(tree)addAttachmentTree('assistant',clean);await persistThread()},log:async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await persistThread()}}
|
||||||
window.USER={log:async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'user',content:[{type:'text',text:t}]});await persistThread()}}
|
window.USER={log:async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'user',content:[{type:'text',text:t}]});await persistThread()}}
|
||||||
const WS_BASE='wss://orp.awww.workers.dev/ws',HTTP_BASE="https://orp.awww.workers.dev/ws"
|
async function init(){threads=await tload();await renderHistory();renderSidebar();reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
|
||||||
|
window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()})
|
||||||
|
init()
|
||||||
|
const WS_BASE='wss://orp.awww.workers.dev/ws'
|
||||||
const buildBody=()=>{const msgs=[];if(store.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:store.masterPrompt}]});if(store.system_prompt)msgs.push({role:'system',content:[{type:'text',text:store.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:store.model,messages:msgs,stream:true});if(store.reasoning_effort&&store.reasoning_effort!=='default')b.reasoning={effort:store.reasoning_effort};if(store.verbosity)b.verbosity=store.verbosity;return b}
|
const buildBody=()=>{const msgs=[];if(store.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:store.masterPrompt}]});if(store.system_prompt)msgs.push({role:'system',content:[{type:'text',text:store.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:store.model,messages:msgs,stream:true});if(store.reasoning_effort&&store.reasoning_effort!=='default')b.reasoning={effort:store.reasoning_effort};if(store.verbosity)b.verbosity=store.verbosity;return b}
|
||||||
async function askOpenRouterStreaming(onDelta,streamId){if(!store.apiKey){onDelta(localDemoReply(),true);return {ok:true,rid:streamId||null}}const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null};const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const userId=getUserId();const ws=new WebSocket(WS_BASE+'?uid='+encodeURIComponent(userId));r.ws=ws;ws.onopen=()=>ws.send(JSON.stringify({type:'begin',rid:r.rid,provider:store.provider,apiKey:store.apiKey,or_body:buildBody()}));ws.onmessage=e=>{let m;try{m=JSON.parse(e.data)}catch{return}if(m.type==='delta'&&typeof m.seq==='number'&&m.seq>r.seq){r.seq=m.seq;onDelta(m.text||'',false)}else if(m.type==='done'){r.done=true;signal('');ws.close()}else if(m.type==='err'){r.done=true;signal('\n\n'+(m.message||'error'));ws.close()}};ws.onclose=()=>{};ws.onerror=()=>{};state.controller={abort:()=>{r.done=true;try{if(ws.readyState===1)ws.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{}signal('')}};return {ok:true,rid:r.rid}}
|
async function askOpenRouterStreaming(onDelta,streamId){if(!store.apiKey){const t=localDemoReply();onDelta(t,true);return {ok:true,rid:streamId||null}}const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null};const signal=t=>{if(!r.signaled){r.signaled=true;onDelta(t||'',true)}};const ws=new WebSocket(WS_BASE+'?uid='+encodeURIComponent(r.rid));r.ws=ws;ws.onopen=()=>ws.send(JSON.stringify({type:'begin',rid:r.rid,provider:store.provider,apiKey:store.apiKey,or_body:buildBody()}));ws.onmessage=e=>{let m;try{m=JSON.parse(e.data)}catch{return}if(m.type==='delta'&&typeof m.seq==='number'&&m.seq>r.seq){r.seq=m.seq;onDelta(m.text||'',false)}else if(m.type==='done'){r.done=true;signal('');ws.close()}else if(m.type==='err'){r.done=true;signal('\n\n'+(m.message||'error'));ws.close()}};ws.onclose=()=>{};ws.onerror=()=>{};state.controller={abort:()=>{r.done=true;try{if(ws.readyState===1)ws.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{}signal('')}};return {ok:true,rid:r.rid}}
|
||||||
function openAccountSettings(){el.set_provider.value=store.provider||'openrouter';el.set_api_key_or.value=store.apiKeyOR||'';el.set_api_key_oai.value=store.apiKeyOAI||'';el.set_master_prompt.value=store.masterPrompt||'';el.accountSettingsModal.classList.remove('hidden')}
|
function openAccountSettings(){el.set_provider.value=store.provider||'openrouter';el.set_api_key_or.value=store.apiKeyOR||'';el.set_api_key_oai.value=store.apiKeyOAI||'';el.set_master_prompt.value=store.masterPrompt||'';el.accountSettingsModal.classList.remove('hidden')}
|
||||||
|
function closeAccountSettings(){el.accountSettingsModal.classList.add('hidden')}
|
||||||
el.accountSettingsOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()})
|
el.accountSettingsOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()})
|
||||||
el.accountSettingsForm.addEventListener('submit',e=>{e.preventDefault();store.provider=el.set_provider.value||'openrouter';store.apiKeyOR=String(el.set_api_key_or.value||'').trim();store.apiKeyOAI=String(el.set_api_key_oai.value||'').trim();store.masterPrompt=String(el.set_master_prompt.value||'').trim();el.accountSettingsModal.classList.add('hidden')})
|
el.closeAccountSettings.addEventListener('click',closeAccountSettings)
|
||||||
async function fetchTranscript(id){try{const r=await fetch(HTTP_BASE+'?uid='+encodeURIComponent(getUserId()));if(!r.ok)return null;return await r.json()}catch{return null}}
|
el.cancelAccountSettings.addEventListener('click',closeAccountSettings)
|
||||||
|
el.accountSettingsModal.addEventListener('click',e=>{if(e.target===el.accountSettingsModal||e.target.classList.contains('bg-black/30'))closeAccountSettings()})
|
||||||
|
el.accountSettingsForm.addEventListener('submit',e=>{e.preventDefault();store.provider=el.set_provider.value||'openrouter';store.apiKeyOR=String(el.set_api_key_or.value||'').trim();store.apiKeyOAI=String(el.set_api_key_oai.value||'').trim();store.masterPrompt=String(el.set_master_prompt.value||'').trim();closeAccountSettings()})
|
||||||
|
const HTTP_BASE = "https://orp.awww.workers.dev/ws"
|
||||||
|
async function fetchTranscript(id){try{const r=await fetch(HTTP_BASE+'?uid='+encodeURIComponent(id));if(!r.ok)return null;return await r.json()}catch{return null}}
|
||||||
const lastAssistantId=()=>{const a=[...el.messages.querySelectorAll('.msg-bubble')];for(let i=a.length-1;i>=0;i--){const b=a[i],h=b.previousElementSibling;if(!h)continue;const you=/^\s*You\b/.test(h.textContent||'');if(!you)return b.dataset.mid||null}return null}
|
const lastAssistantId=()=>{const a=[...el.messages.querySelectorAll('.msg-bubble')];for(let i=a.length-1;i>=0;i--){const b=a[i],h=b.previousElementSibling;if(!h)continue;const you=/^\s*You\b/.test(h.textContent||'');if(!you)return b.dataset.mid||null}return null}
|
||||||
const getBubbleById=id=>el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`)
|
const getBubbleById=id=>el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`)
|
||||||
async function syncTranscript(){const id=lastAssistantId();if(!id)return false;const bubble=getBubbleById(id);if(!bubble)return false;const j=await fetchTranscript(id).catch(()=>null);if(!j||j.rid!==id){if(j&&j.error){setBtnSend();state.busy=false;const t=(bubble.textContent||'')+'\n\n'+j.error;renderMarkdown(bubble,t,{enhance:false});enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread()}return false}const t=j.text||'';renderMarkdown(bubble,t,{enhance:false});if(j.error||j.done||j.phase==='done'){setBtnSend();state.busy=false;enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread();return false}return true}
|
async function syncTranscript(){const id=lastAssistantId();if(!id)return false;const bubble=getBubbleById(id);if(!bubble)return false;const j=await fetchTranscript(id).catch(()=>null);if(!j||j.rid!==id){if(j&&j.error){setBtnSend();state.busy=false;const t=(bubble.textContent||'')+'\n\n'+j.error;renderMarkdown(bubble,t,{enhance:false});enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread()}return false}const t=j.text||'';renderMarkdown(bubble,t,{enhance:false});if(j.error||j.done||j.phase==='done'){setBtnSend();state.busy=false;enhanceCodeBlocks(bubble,true);const i=state.messages.findIndex(m=>m.id===id);if(i>=0)state.messages[i].content=[{type:'text',text:t}];else state.messages.push({id,role:'assistant',content:[{type:'text',text:t}]});persistThread();return false}return true}
|
||||||
let syncLoopRunning=false
|
let syncLoopRunning=false
|
||||||
const syncKick=r=>{if(!state.busy||document.visibilityState==='hidden')return;setTimeout(()=>syncLoop(r),0)}
|
const syncKick=r=>{if(!state.busy||document.visibilityState==='hidden')return;setTimeout(()=>syncLoop(r),0)}
|
||||||
async function syncLoop(){if(!state.busy||syncLoopRunning)return;const id=lastAssistantId();if(!id)return;syncLoopRunning=true;try{for(;;){if(!state.busy)break;const cont=await syncTranscript().catch(()=>false);if(!cont)break;await new Promise(r=>setTimeout(r,1200))}}finally{syncLoopRunning=false}}
|
async function syncLoop(){if(!state.busy||syncLoopRunning)return;const id=lastAssistantId();if(!id)return;syncLoopRunning=true;try{for(;;){if(!state.busy)break;const cont=await syncTranscript().catch(()=>false);if(!cont)break;await new Promise(r=>setTimeout(r,1200))}}finally{syncLoopRunning=false}}
|
||||||
async function recoverLastRun(){try{const resp=await fetch(HTTP_BASE+'?uid='+encodeURIComponent(getUserId()));if(!resp.ok)return;const remoteRun=await resp.json();if(remoteRun&&remoteRun.rid&&(remoteRun.phase==='done'||remoteRun.phase==='error')){if(threads.length>0){const latestThread=[...threads].sort((a,b)=>b.updatedAt-a.updatedAt)[0];const messages=latestThread.messages||[];const alreadyExists=messages.some(m=>m.id===remoteRun.rid);if(!alreadyExists){console.log(`Recovering lost message: ${remoteRun.rid}`);const newMsg={id:remoteRun.rid,role:'assistant',sune_name:'Unknown',model:'Unknown',content:[{type:'text',text:remoteRun.text+(remoteRun.error?`\n\nError: ${remoteRun.error}`:'')}]};latestThread.messages.push(newMsg);latestThread.updatedAt=Date.now();await tsave(threads);await renderHistory();if(!state.currentThreadId||state.currentThreadId===latestThread.id){state.currentThreadId=latestThread.id;clearChat();state.messages=[...latestThread.messages];for(const m of state.messages){const b=msgRow(m);b.dataset.mid=m.id||'';renderMarkdown(b,partsToText(m.content))}}}}}}catch(e){console.error("Failed to recover last run:",e)}}
|
;['focus','pageshow'].forEach(ev=>window.addEventListener(ev,()=>syncKick(ev)))
|
||||||
const setupModal=(modal,closeBtn,cancelBtn)=>{const close=()=>modal.classList.add('hidden');closeBtn.addEventListener('click',close);if(cancelBtn)cancelBtn.addEventListener('click',close);modal.addEventListener('click',e=>{if(e.target===modal||e.target.classList.contains('bg-black/30'))close()})};
|
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')syncKick('visible')})
|
||||||
const addCpPaste=(copyId,pasteId,getter,setter)=>{el[copyId].addEventListener('click',async()=>{try{await navigator.clipboard.writeText(getter())}catch{}});el[pasteId].addEventListener('click',async()=>{try{await setter(await navigator.clipboard.readText())}catch{}})};
|
el.copySystemPrompt.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}})
|
||||||
async function init(){
|
el.pasteSystemPrompt.addEventListener('click',async()=>{try{el.set_system_prompt.value=await navigator.clipboard.readText()}catch{}})
|
||||||
threads=await tload();
|
el.copyHTML.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.htmlEditor.textContent||'')}catch{}})
|
||||||
renderHistory();
|
el.pasteHTML.addEventListener('click',async()=>{try{const t=await navigator.clipboard.readText();const j=(await ensureJars()).html;j.updateCode?j.updateCode(t):el.htmlEditor.textContent=t}catch{}})
|
||||||
renderSidebar();
|
el.copyJS.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(el.scriptEditor.textContent||'')}catch{}})
|
||||||
reflectActiveSune();
|
el.pasteJS.addEventListener('click',async()=>{try{const t=await navigator.clipboard.readText();const j=(await ensureJars()).js;j.updateCode?j.updateCode(t):el.scriptEditor.textContent=t}catch{}})
|
||||||
clearChat();
|
|
||||||
icons();
|
|
||||||
kbBind();
|
|
||||||
kbUpdate();
|
|
||||||
setupModal(el.settingsModal,el.closeSettings,el.cancelSettings);
|
|
||||||
setupModal(el.accountSettingsModal,el.closeAccountSettings,el.cancelAccountSettings);
|
|
||||||
addCpPaste('copySystemPrompt','pasteSystemPrompt',()=>el.set_system_prompt.value||'',text=>el.set_system_prompt.value=text);
|
|
||||||
addCpPaste('copyHTML','pasteHTML',()=>el.htmlEditor.textContent||'',async text=>{const j=(await ensureJars()).html;j.updateCode?j.updateCode(text):(el.htmlEditor.textContent=text)});
|
|
||||||
addCpPaste('copyJS','pasteJS',()=>el.scriptEditor.textContent||'',async text=>{const j=(await ensureJars()).js;j.updateCode?j.updateCode(text):(el.scriptEditor.textContent=text)});
|
|
||||||
document.body.addEventListener('click',e=>{const target=e.target;if(!el.historyMenu.contains(target)&&!target.closest('[data-thread-menu]'))hideHistoryMenu();if(!el.suneMenu.contains(target)&&!target.closest('[data-sune-menu]'))hideSuneMenu();if(!el.userMenu.contains(target)&&!el.userMenuBtn.contains(target))el.userMenu.classList.add('hidden')});
|
|
||||||
window.addEventListener('resize',()=>{hideHistoryMenu();hideSuneMenu()});
|
|
||||||
['focus','pageshow'].forEach(ev=>window.addEventListener(ev,()=>syncKick(ev)))
|
|
||||||
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')syncKick('visible')})
|
|
||||||
await recoverLastRun();
|
|
||||||
}
|
|
||||||
init()
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
10
readme.md
@@ -1,18 +1,18 @@
|
|||||||
.
|

|
||||||
|
|
||||||
> Each sune is like a module. You can have many. And share them.
|
> Each sune is like a module. You can have many. And share them.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
💠 New!
|
💠 New!
|
||||||
|
|
||||||
> You can have scripts which run on the page of each sune — either to function call or extend functionality of the app or sune.
|
> You can have scripts which run on the page of each sune — either to function call or extend functionality of the app or sune.
|
||||||
|
|
||||||

|

|
||||||
Here is an example of some HTML/JS artwork.
|
Here is an example of some HTML/JS artwork.
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
Sune function call (have Sunes attach files to their latest bubble)
|
|
||||||
|
|
||||||
In your Sune script, call:
|
|
||||||
```js
|
|
||||||
await window.suneAttach(files, { toAPI: false, tree: true })
|
|
||||||
```
|
|
||||||
files can be File objects or simple objects like:
|
|
||||||
```js
|
|
||||||
{ name: 'report.pdf', mime: 'application/pdf', data: '<BASE64>', size: 123456 }
|
|
||||||
```
|
|
||||||
`toAPI: true` adds an assistant message containing the actual file parts (so the API can consume them next turn).
|
|
||||||
|
|
||||||
`tree: true` adds the separate Attachments tree bubble with clickable downloads.
|
|
||||||
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
```js
|
|
||||||
await window.suneAttach([fileInput.files[0]])
|
|
||||||
await window.suneAttach([{name:'cat.png',mime:'image/png',data:'<BASE64>'}])
|
|
||||||
```
|
|
||||||
If you want only the tree bubble and not re-send data to the API, set {toAPI:false, tree:true}.
|
|
||||||
@@ -36,4 +36,4 @@ const html = createHtmlPlugin({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default defineConfig({ build: { outDir: 'docs', minify: false }, plugins: [pwa, html] })
|
export default defineConfig({ build: { minify: false }, plugins: [pwa, html] })
|
||||||
|
|||||||