mirror of
https://github.com/multipleof4/sune.git
synced 2026-09-18 11:35:43 +00:00
Compare commits
4 Commits
66a9921d86
...
4a16233c23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a16233c23 | ||
| 731b6a2dcc | |||
| 1365e6c4ca | |||
| 2b9a7b4c25 |
@@ -385,12 +385,45 @@ var imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => {
|
||||
var b64 = (x) => x.split(",")[1] || "";
|
||||
var utob = (s) => btoa(unescape(encodeURIComponent(s)));
|
||||
var btou = (s) => decodeURIComponent(escape(atob(s.replace(/\s/g, ""))));
|
||||
function partsToText(m) {
|
||||
async function copyToClipboard(text) {
|
||||
if (typeof text !== "string") text = String(text ?? "");
|
||||
if (navigator.clipboard?.writeText) try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch {}
|
||||
try {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.opacity = "0";
|
||||
ta.style.left = "-9999px";
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
const ok = document.execCommand("copy");
|
||||
ta.remove();
|
||||
return ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function partsToText(m, stripData = false) {
|
||||
if (!m) return "";
|
||||
const c = m.content, i = m.images;
|
||||
let t = Array.isArray(c) ? c.map((p) => p?.type === "text" ? p.text : p?.type === "image_url" ? `` : p?.type === "file" ? `[${p.file?.filename || "file"}]` : p?.type === "input_audio" ? `(audio:${p.input_audio?.format || ""})` : "").join("\n") : String(c || "");
|
||||
if (Array.isArray(i)) t += i.map((x) => `\n\n`).join("");
|
||||
return t;
|
||||
const c = m.content, i = m.images, out = [];
|
||||
if (Array.isArray(c)) {
|
||||
for (const p of c) if (p?.type === "text") {
|
||||
if (p.text) out.push(p.text);
|
||||
} else if (p?.type === "image_url") {
|
||||
const u = p.image_url?.url || "";
|
||||
if (!stripData || !u.startsWith("data:")) out.push(``);
|
||||
} else if (p?.type === "file") out.push(`[${p.file?.filename || "file"}]`);
|
||||
else if (p?.type === "input_audio") out.push(`(audio:${p.input_audio?.format || ""})`);
|
||||
} else if (c != null) out.push(String(c));
|
||||
if (Array.isArray(i)) for (const x of i) {
|
||||
const u = x.image_url?.url || "";
|
||||
if (!stripData || !u.startsWith("data:")) out.push(``);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
function dl(name, obj) {
|
||||
const blob = new Blob([JSON.stringify(obj, null, 2)], { type: name.endsWith(".sune") ? "application/octet-stream" : "application/json" }), url = URL.createObjectURL(blob), a = document.createElement("a");
|
||||
@@ -575,11 +608,10 @@ function enhanceCodeBlocks(root, doHL = true) {
|
||||
const len = code.textContent.length, countText = len >= 1e3 ? (len / 1e3).toFixed(1) + "K" : len;
|
||||
const $btn = window.$("<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);
|
||||
if (await copyToClipboard(code.innerText)) {
|
||||
$btn.text("Copied");
|
||||
setTimeout(() => $btn.text("Copy"), 1200);
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
const $container = window.$("<div class=\"code-actions absolute top-2 right-2 flex items-center gap-2\"></div>");
|
||||
$container.append(window.$(`<span class="text-xs text-gray-500">${countText} chars</span>`), $btn);
|
||||
@@ -1123,15 +1155,14 @@ function _createMessageRow(m) {
|
||||
});
|
||||
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));
|
||||
if (await copyToClipboard(partsToText(state.messages.find((x) => x.id === m.id) || m, true))) {
|
||||
$(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>`);
|
||||
@@ -1485,10 +1516,7 @@ $(el.threadPopover).on("click", async (e) => {
|
||||
const u = el.threadRepoInput.value.trim();
|
||||
if (u.startsWith("gh://")) {
|
||||
const info = parseGhUrl(u);
|
||||
try {
|
||||
await navigator.clipboard.writeText(`${info.owner}/${info.repo}@${info.branch}/${th.id}`);
|
||||
alert("Path copied.");
|
||||
} catch {}
|
||||
if (await copyToClipboard(`${info.owner}/${info.repo}@${info.branch}/${th.id}`)) alert("Path copied.");
|
||||
}
|
||||
}
|
||||
hideThreadPopover();
|
||||
@@ -2355,11 +2383,7 @@ var onForeground = () => {
|
||||
if (state.busy) syncWhileBusy();
|
||||
};
|
||||
$(document).on("visibilitychange", onForeground);
|
||||
$(el.copySystemPrompt).on("click", async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(el.set_system_prompt.value || "");
|
||||
} catch {}
|
||||
});
|
||||
$(el.copySystemPrompt).on("click", async () => await copyToClipboard(el.set_system_prompt.value || ""));
|
||||
$(el.pasteSystemPrompt).on("click", async () => {
|
||||
try {
|
||||
el.set_system_prompt.value = await navigator.clipboard.readText();
|
||||
@@ -2367,10 +2391,8 @@ $(el.pasteSystemPrompt).on("click", async () => {
|
||||
});
|
||||
var getActiveJar = () => !el.htmlEditor.classList.contains("hidden") ? jars.html : jars.extension;
|
||||
$(el.copyHTML).on("click", async () => {
|
||||
try {
|
||||
const jar = getActiveJar();
|
||||
await navigator.clipboard.writeText(jar ? jar.toString() : "");
|
||||
} catch {}
|
||||
const jar = getActiveJar();
|
||||
await copyToClipboard(jar ? jar.toString() : "");
|
||||
});
|
||||
$(el.pasteHTML).on("click", async () => {
|
||||
try {
|
||||
@@ -2405,6 +2427,7 @@ Object.assign(window, {
|
||||
_createMessageRow,
|
||||
msgRow,
|
||||
partsToText,
|
||||
copyToClipboard,
|
||||
addSuneBubbleStreaming,
|
||||
clearChat,
|
||||
payloadWithSampling,
|
||||
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -13,7 +13,7 @@
|
||||
<script defer src="//unpkg.com/alpinejs"></script>
|
||||
|
||||
|
||||
<script type="module" crossorigin src="/assets/index-b2v8Ytf5.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DgFnfrFO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DUC1RW1F.css">
|
||||
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
|
||||
<body class="bg-white text-gray-900 selection:bg-black/10" x-data @click.window="if($event.target.closest('button')) haptic(); if(!document.getElementById('threadPopover').contains($event.target)&&!$event.target.closest('[data-thread-menu]')) hideThreadPopover(); if(!document.getElementById('sunePopover').contains($event.target)&&!$event.target.closest('[data-sune-menu]')) hideSunePopover(); if(!document.getElementById('userMenu').contains($event.target)&&!document.getElementById('userMenuBtn').contains($event.target)) document.getElementById('userMenu').classList.add('hidden')">
|
||||
|
||||
2
dist/sw.js
vendored
2
dist/sw.js
vendored
@@ -1 +1 @@
|
||||
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,t)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(s[r])return;let o={};const l=e=>i(e,r),c={module:{uri:r},exports:o,require:l};s[r]=Promise.all(n.map(e=>c[e]||l(e))).then(e=>(t(...e),o))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"index.html",revision:"5479938ea7e8721c4c76398f2d94d0f1"},{url:"assets/index-b2v8Ytf5.js",revision:null},{url:"assets/index-DUC1RW1F.css",revision:null},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
||||
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const d=e=>i(e,t),l={module:{uri:t},exports:o,require:d};s[t]=Promise.all(n.map(e=>l[e]||d(e))).then(e=>(r(...e),o))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"index.html",revision:"0b0cd45d08b40d617d4906d0b3b35466"},{url:"assets/index-DgFnfrFO.js",revision:null},{url:"assets/index-DUC1RW1F.css",revision:null},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
|
||||
|
||||
12
src/main.js
12
src/main.js
@@ -3,7 +3,7 @@ import {SUNE_LOGO_SVG} from './sune-logo.js';
|
||||
import {STICKY_SUNES} from './sticky-sunes.js';
|
||||
import {generateTitleWithAI} from './title-generator.js';
|
||||
import { el } from './dom.js';
|
||||
import { clamp, num, int, gid, esc, positionPopover, sid, fmtSize, asDataURL, imgToWebp, b64, utob, btou, dl, ts, partsToText } from './utils.js';
|
||||
import { clamp, num, int, gid, esc, positionPopover, sid, fmtSize, asDataURL, imgToWebp, b64, utob, btou, dl, ts, partsToText, copyToClipboard } from './utils.js';
|
||||
import { ghApi, parseGhUrl, ghGetFileContent } from './github.js';
|
||||
import { USER } from './user.js';
|
||||
import { md, enhanceCodeBlocks, renderMarkdown } from './markdown.js';
|
||||
@@ -30,7 +30,7 @@ const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pin
|
||||
const renderSidebar=window.renderSidebar=()=>{const list=[...SUNE.list].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()}
|
||||
const renderUserUI=window.renderUserUI=()=>{if(!el.userMenuAvatar)return;const a=USER.avatar;if(a){el.userMenuAvatar.className='h-6 w-6 rounded-full overflow-hidden shrink-0';el.userMenuAvatar.innerHTML=`<img src="${esc(a)}" class="h-full w-full object-cover"/>`}else{el.userMenuAvatar.className='h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center shrink-0 text-xs';el.userMenuAvatar.textContent='👤'}}
|
||||
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 _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();const cur=state.messages.find(x=>x.id===m.id)||m;if(await copyToClipboard(partsToText(cur,true))){$(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)}});$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]}
|
||||
const addMessage=window.addMessage=function(m,track=true){m.id=m.id||gid();if(!Array.isArray(m.content)&&m.content!=null){m.content=[{type:'text',text:String(m.content)}]}const bubble=msgRow(m);bubble.dataset.mid=m.id;renderMarkdown(bubble,partsToText(m));if(track)state.messages.push(m);if(m.role==='assistant')el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:m}}));return bubble}
|
||||
const addSuneBubbleStreaming=(meta,id)=>msgRow(Object.assign({role:'assistant',id},meta))
|
||||
@@ -71,7 +71,7 @@ $(el.threadList).on('scroll',()=>{
|
||||
}
|
||||
isAddingThreads=false;
|
||||
});
|
||||
$(el.threadPopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=THREAD.get(menuThreadId);if(!th)return;const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';if(act==='pin'){th.pinned=!th.pinned;if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}}else if(act==='duplicate'){const newId=gid(),msgs=await localforage.getItem(prefix+th.id)||[];const newTh={...th,id:newId,title:th.title+' (Copy)',updatedAt:Date.now()};if(u.startsWith('gh://'))newTh.status='new';THREAD.list.unshift(newTh);await localforage.setItem(prefix+newId,msgs);await THREAD.save();await renderThreads()}else if(act==='delete'){if(confirm('Delete this chat?')){if(u.startsWith('gh://')){th.status='deleted';th.updatedAt=Date.now()}else{THREAD.list=THREAD.list.filter(x=>!th.id!==th.id);await localforage.removeItem(prefix+th.id)}if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=await localforage.getItem(prefix+th.id)||[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m)||'').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+')')}else if(act==='export'){const msgs=await localforage.getItem(prefix+th.id)||[];dl(`thread-${(th.title||'thread').replace(/\W/g,'_')}-${ts()}.json`,{...th,messages:msgs})}else if(act==='copy_path'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);try{await navigator.clipboard.writeText(`${info.owner}/${info.repo}@${info.branch}/${th.id}`);alert('Path copied.')}catch{}}}hideThreadPopover();await THREAD.save();renderThreads()})
|
||||
$(el.threadPopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=THREAD.get(menuThreadId);if(!th)return;const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';if(act==='pin'){th.pinned=!th.pinned;if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}}else if(act==='duplicate'){const newId=gid(),msgs=await localforage.getItem(prefix+th.id)||[];const newTh={...th,id:newId,title:th.title+' (Copy)',updatedAt:Date.now()};if(u.startsWith('gh://'))newTh.status='new';THREAD.list.unshift(newTh);await localforage.setItem(prefix+newId,msgs);await THREAD.save();await renderThreads()}else if(act==='delete'){if(confirm('Delete this chat?')){if(u.startsWith('gh://')){th.status='deleted';th.updatedAt=Date.now()}else{THREAD.list=THREAD.list.filter(x=>!th.id!==th.id);await localforage.removeItem(prefix+th.id)}if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=await localforage.getItem(prefix+th.id)||[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m)||'').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+')')}else if(act==='export'){const msgs=await localforage.getItem(prefix+th.id)||[];dl(`thread-${(th.title||'thread').replace(/\W/g,'_')}-${ts()}.json`,{...th,messages:msgs})}else if(act==='copy_path'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);if(await copyToClipboard(`${info.owner}/${info.repo}@${info.branch}/${th.id}`))alert('Path copied.')}}hideThreadPopover();await THREAD.save();renderThreads()})
|
||||
$(el.suneList).on('click',async e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSunePopover(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){if(state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=false;state.controller=null};SUNE.setActive(id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')}})
|
||||
$(el.sunePopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=SUNE.get(menuSuneId);if(!s)return;const updateAndRender=async()=>{s.updatedAt=Date.now();SUNE.save();renderSidebar();await reflectActiveSune()};if(act==='pin'){s.pinned=!s.pinned;await updateAndRender()}else if(act==='rename'){const n=prompt('Rename sune to:',s.name);if(n!=null){s.name=n.trim();await updateAndRender()}}else if(act==='pfp'){const i=document.createElement('input');i.type='file';i.accept='image/*';i.onchange=async()=>{const f=i.files?.[0];if(!f)return;try{s.avatar=await imgToWebp(f);await updateAndRender()}catch{}};i.click()}else if(act==='export')dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${ts()}.sune`,[s]);hideSunePopover()})
|
||||
function updateAttachBadge(){const n=state.attachments.length;el.attachBadge.textContent=String(n);el.attachBadge.classList.toggle('hidden',n===0)}
|
||||
@@ -174,10 +174,10 @@ let syncLoopRunning=false
|
||||
async function syncWhileBusy(){if(syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=true;try{while(await syncActiveThread())await new Promise(r=>setTimeout(r,1500))}finally{syncLoopRunning=false}}
|
||||
const onForeground=()=>{if(document.visibilityState!=='visible')return;state.controller?.disconnect?.();if(state.busy)syncWhileBusy()}
|
||||
$(document).on('visibilitychange',onForeground)
|
||||
$(el.copySystemPrompt).on('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}})
|
||||
$(el.copySystemPrompt).on('click',async()=>await copyToClipboard(el.set_system_prompt.value||''))
|
||||
$(el.pasteSystemPrompt).on('click',async()=>{try{el.set_system_prompt.value=await navigator.clipboard.readText()}catch{}})
|
||||
const getActiveJar=()=>!el.htmlEditor.classList.contains('hidden')?jars.html:jars.extension
|
||||
$(el.copyHTML).on('click',async()=>{try{const jar=getActiveJar();await navigator.clipboard.writeText(jar?jar.toString():'')}catch{}})
|
||||
$(el.copyHTML).on('click',async()=>{const jar=getActiveJar();await copyToClipboard(jar?jar.toString():'')})
|
||||
$(el.pasteHTML).on('click',async()=>{try{const t=await navigator.clipboard.readText();const jar=getActiveJar();if(jar)jar.updateCode(t)}catch{}})
|
||||
|
||||
Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,renderUserUI,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,getActiveJar,imgToWebp,cacheStore,ghApi,parseGhUrl,ghGetFileContent,pullThreads});
|
||||
Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,renderUserUI,enhanceCodeBlocks,getSuneLabel,_createMessageRow,msgRow,partsToText,copyToClipboard,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,getActiveJar,imgToWebp,cacheStore,ghApi,parseGhUrl,ghGetFileContent,pullThreads});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import mathjax3 from 'https://esm.sh/markdown-it-mathjax3';
|
||||
import { copyToClipboard } from './utils.js';
|
||||
|
||||
export const md = window.md = window.markdownit({ html: false, linkify: true, typographer: true, breaks: true }).use(mathjax3);
|
||||
|
||||
@@ -10,11 +11,10 @@ export function enhanceCodeBlocks(root, doHL = true) {
|
||||
const len = code.textContent.length, countText = len >= 1e3 ? (len / 1e3).toFixed(1) + 'K' : len;
|
||||
const $btn = window.$('<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);
|
||||
if (await copyToClipboard(code.innerText)) {
|
||||
$btn.text('Copied');
|
||||
setTimeout(() => $btn.text('Copy'), 1200);
|
||||
} catch { }
|
||||
}
|
||||
});
|
||||
const $container = window.$('<div class="code-actions absolute top-2 right-2 flex items-center gap-2"></div>');
|
||||
$container.append(window.$(`<span class="text-xs text-gray-500">${countText} chars</span>`), $btn);
|
||||
|
||||
50
src/utils.js
50
src/utils.js
@@ -33,12 +33,52 @@ export const b64 = x => x.split(',')[1] || '';
|
||||
export const utob = s => btoa(unescape(encodeURIComponent(s)));
|
||||
export const btou = s => decodeURIComponent(escape(atob(s.replace(/\s/g, ''))));
|
||||
|
||||
export function partsToText(m) {
|
||||
export async function copyToClipboard(text) {
|
||||
if (typeof text !== 'string') text = String(text ?? '');
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try { await navigator.clipboard.writeText(text); return true; } catch {}
|
||||
}
|
||||
try {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
ta.style.left = '-9999px';
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
const ok = document.execCommand('copy');
|
||||
ta.remove();
|
||||
return ok;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
export function partsToText(m, stripData = false) {
|
||||
if (!m) return '';
|
||||
const c = m.content, i = m.images;
|
||||
let t = Array.isArray(c) ? c.map(p => p?.type === 'text' ? p.text : (p?.type === 'image_url' ? `` : (p?.type === 'file' ? `[${p.file?.filename || 'file'}]` : (p?.type === 'input_audio' ? `(audio:${p.input_audio?.format || ''})` : '')))).join('\n') : String(c || '');
|
||||
if (Array.isArray(i)) t += i.map(x => `\n\n`).join('');
|
||||
return t;
|
||||
const c = m.content, i = m.images, out = [];
|
||||
if (Array.isArray(c)) {
|
||||
for (const p of c) {
|
||||
if (p?.type === 'text') {
|
||||
if (p.text) out.push(p.text);
|
||||
} else if (p?.type === 'image_url') {
|
||||
const u = p.image_url?.url || '';
|
||||
if (!stripData || !u.startsWith('data:')) out.push(``);
|
||||
} else if (p?.type === 'file') {
|
||||
out.push(`[${p.file?.filename || 'file'}]`);
|
||||
} else if (p?.type === 'input_audio') {
|
||||
out.push(`(audio:${p.input_audio?.format || ''})`);
|
||||
}
|
||||
}
|
||||
} else if (c != null) {
|
||||
out.push(String(c));
|
||||
}
|
||||
if (Array.isArray(i)) {
|
||||
for (const x of i) {
|
||||
const u = x.image_url?.url || '';
|
||||
if (!stripData || !u.startsWith('data:')) out.push(``);
|
||||
}
|
||||
}
|
||||
return out.join('\n');
|
||||
}
|
||||
|
||||
export function dl(name, obj) {
|
||||
|
||||
Reference in New Issue
Block a user