1 Commits

Author SHA1 Message Date
github-actions[bot]
c7c843eb5a This build was committed by a bot. 2026-09-01 00:25:13 +00:00
18 changed files with 127 additions and 575 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
github: multipleof4
custom: https://paypal.me/planetrenox

View File

@@ -1,2 +0,0 @@
public/
dist/

85
LICENSE
View File

@@ -1,85 +0,0 @@
1. Scope
The copyright holders offering software, source code, assets, or other content
under these terms ("we") grant the permissions below to every individual, team,
company, organization, and other entity ("you"). "Work" means the software,
source code, assets, and other content we offer under these terms.
2. Using the product and owning your output
You may use the customer-facing software or product for any purpose, including
commercial purposes, without payment, acknowledgment, or a copy of these terms.
This includes installing, running, accessing, and self-hosting it, and compiling
it solely to run it.
Whatever you create through ordinary use of the product is yours. We claim no
ownership of your output and impose no payment, acknowledgment, or licensing
conditions on it. You may use, sell, share, or license it however you choose.
Merely copying or extracting the underlying codebase or assetbase is reuse
under section 3.
3. Reusing code or assets
Subject to section 4, you have worldwide, nonexclusive permission to copy,
modify, combine, publish, distribute, sublicense, sell, and otherwise reuse any
or all of the Work, for any purpose, in source or other forms.
4. Finding common ground
If you reuse the codebase or assetbase, you are responsible for satisfying one
of the following conditions when you begin that reuse:
- If you cannot reasonably afford a monetary payment, give a simple
acknowledgment of the original software or repository in an appropriate
place, such as your README, source code, website, or documentation. A name
mention or a link is enough. No particular wording or placement is required.
- If you can afford a monetary payment, sponsor the original
repository through its Sponsor button. You alone decide the minimum
reasonable amount you can afford, considering the circumstances of the entity
reusing the Work. Sponsorship may be one-time or monthly. If you pay any
positive amount, acknowledgment and links are optional.
An entity may satisfy this condition through someone acting on its behalf.
5. Final settlement
Any positive amount successfully paid through that sponsorship route fully
and permanently satisfies the reuse condition for the entity making it or on
whose behalf it is made. Your chosen amount is accepted as sufficient,
regardless of your financial means.
We will not challenge its adequacy, request financial evidence, or demand a
larger amount. A record of the completed payment is sufficient proof; no
separate approval or agreement is needed.
The first completed monthly payment is enough. Later payments are optional,
and you may cancel recurring sponsorship without losing your permissions.
Likewise, an appropriate acknowledgment fully satisfies the condition for an
entity using the acknowledgment option.
Once satisfied, the condition covers all reuse by that entity of the Work
offered under these terms in the same original repository, including later
versions. No payment or acknowledgment is required again for additional uses,
copies, releases, or products. The permissions granted are then irrevocable
for that Work.
6. Choosing another license
After satisfying section 4, you may distribute or sublicense the Work,
modified or unmodified, under any license you choose. You do not have to include
these terms, keep this license, or require anyone else to follow it. These
terms require no retained copyright or license notices; the acknowledgment
option in section 4 is the only acknowledgment requirement.
Recipients of the Work you distribute under another license follow that
license and owe us no payment or acknowledgment under these terms for that
Work. Relicensing grants permissions; it does not transfer ownership of
the original Work.
7. Warranty
To the extent permitted by law, the Work is provided "as is," without any
warranty, including merchantability, fitness for a particular purpose, or
noninfringement. We are not liable for any claim, loss, or damages arising from
the Work or its use.

View File

@@ -40,8 +40,7 @@ var buildBody = () => {
role: "system",
content: mPrompt
});
const modelName = SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/, "").replace(/^[^/]*\//, "").replace(/(?::online|:free)+$/, "");
const sPrompt = (SUNE.system_prompt || "").trim().replaceAll("{{model_name}}", () => modelName);
const sPrompt = (SUNE.system_prompt || "").trim();
if (sPrompt) msgs.push({
role: "system",
content: sPrompt
@@ -71,6 +70,7 @@ var buildBody = () => {
...SUNE.reasoning_effort && SUNE.reasoning_effort !== "default" ? { effort: SUNE.reasoning_effort } : {},
exclude: !SUNE.include_thoughts
};
if (SUNE.verbosity) b.verbosity = SUNE.verbosity;
if (SUNE.img_output) {
b.modalities = ["image"];
b.image_config = {
@@ -250,7 +250,7 @@ var el = window.el = Object.fromEntries([
"set_repetition_penalty",
"set_min_p",
"set_top_a",
"set_max_tokens",
"set_verbosity",
"set_reasoning_effort",
"set_system_prompt",
"set_hide_composer",
@@ -326,11 +326,7 @@ var el = window.el = Object.fromEntries([
"threadFolderBtn",
"threadSyncBtn",
"suneRepoInput",
"suneSyncBtn",
"suneSyncBadge",
"sunesSyncPopover",
"sunesSyncUploadBtn",
"sunesSyncDownloadBtn"
"suneSyncBtn"
].map((id) => [id, document.getElementById(id)]));
//#endregion
//#region src/utils.js
@@ -372,7 +368,7 @@ var asDataURL = (f) => new Promise((r) => {
fr.onload = () => r(String(fr.result || ""));
fr.readAsDataURL(f);
});
var imgToWebp = (f, D = 96, q = 75) => new Promise((r, j) => {
var imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => {
if (!f) return j();
const i = new Image();
i.onload = () => {
@@ -606,7 +602,6 @@ var md = window.md = window.markdownit({
typographer: true,
breaks: true
}).use(mathjax3);
md.linkify.set({ fuzzyLink: true });
function enhanceCodeBlocks(root, doHL = true) {
window.$(root).find("pre>code").each((i, code) => {
if (code.textContent.length > 2e5) return;
@@ -646,11 +641,6 @@ function kbUpdate() {
el.chat.style.scrollPaddingBottom = fh + overlap + 16 + "px";
}
function kbBind() {
el.input.addEventListener("keydown", (e) => {
if (e.key !== "Enter" || e.shiftKey || e.isComposing || e.keyCode === 229 || e.repeat || !matchMedia("(hover: hover) and (pointer: fine)").matches) return;
e.preventDefault();
el.composer.requestSubmit();
});
if (window.visualViewport) ["resize", "scroll"].forEach((ev) => window.visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true }));
window.$(window).on("resize orientationchange", () => setTimeout(kbUpdate, 50));
window.$(el.input).on("focus click", () => {
@@ -892,7 +882,6 @@ var SYSTEM_KEYS = new Set([
"active_sune_id",
"thread_repo_url",
"sune_repo_url",
"sunes_updated_at",
"user_name",
"user_avatar",
"provider",
@@ -924,49 +913,6 @@ var cleanSuneStorage = (id) => {
if (k.startsWith(p)) localStorage.removeItem(k);
});
};
var isSyncPulling = false;
var getLocalSunesUpdatedAt = () => num(localStorage.getItem("sunes_updated_at"), 0);
var setLocalSunesUpdatedAt = (ts = Date.now()) => localStorage.setItem("sunes_updated_at", ts);
var markLocalDirty = () => {
if (isSyncPulling) return;
setLocalSunesUpdatedAt();
setSuneSyncStatus("desynced");
};
var isSuneStorageKey = (k) => !SYSTEM_KEYS.has(k) && /^sune_[^_]+_/.test(k);
var _origSetItem = localStorage.setItem.bind(localStorage), _origRemoveItem = localStorage.removeItem.bind(localStorage);
localStorage.setItem = (k, v) => {
_origSetItem(k, v);
if (!isSyncPulling && isSuneStorageKey(k)) markLocalDirty();
};
localStorage.removeItem = (k) => {
_origRemoveItem(k);
if (!isSyncPulling && isSuneStorageKey(k)) markLocalDirty();
};
function setSuneSyncStatus(status) {
const b = el.suneSyncBadge;
if (!b) return;
if (!(el.suneRepoInput?.value || "").trim().startsWith("gh://")) {
b.className = "hidden";
return;
}
b.className = "absolute -top-1 -right-1 block h-2.5 w-2.5 rounded-full ring-2 ring-white";
switch (status) {
case "checking":
b.classList.add("bg-blue-500", "animate-pulse");
break;
case "synced":
b.classList.add("bg-green-500");
break;
case "desynced":
b.classList.add("bg-amber-500");
break;
case "error":
b.classList.add("bg-red-500");
break;
default: b.classList.add("bg-gray-400");
}
el.suneSyncBtn?.querySelector("svg")?.classList.toggle("animate-spin", status === "checking");
}
var suneStorage = {
get(k, def = null) {
const id = SUNE.id;
@@ -1031,8 +977,8 @@ var defaultSettings = {
repetition_penalty: "",
min_p: "",
top_a: "",
max_tokens: "",
reasoning_effort: "",
verbosity: "",
reasoning_effort: "default",
system_prompt: "",
html: "",
extension_html: "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",
@@ -1070,7 +1016,6 @@ var SUNE = window.SUNE = new Proxy({
const s = makeSune(p);
sunes.unshift(s);
su.save(sunes);
markLocalDirty();
return s;
},
delete(id) {
@@ -1079,7 +1024,6 @@ var SUNE = window.SUNE = new Proxy({
su.save(sunes);
cleanSuneStorage(id);
gcStorage();
markLocalDirty();
if (sunes.length === 0) {
const def = this.create({ name: "Default" });
this.setActive(def.id);
@@ -1218,7 +1162,6 @@ var SUNE = window.SUNE = new Proxy({
target[p] = value;
sunes[i].updatedAt = Date.now();
su.save(sunes);
markLocalDirty();
}
return true;
}
@@ -1249,7 +1192,7 @@ var getModelShort = (m) => {
var reflectActiveSune = async () => {
const a = SUNE.active;
el.suneBtnTop.title = `Settings — ${a.name}`;
el.suneBtnTop.innerHTML = a.avatar ? `<img src="${esc(a.avatar)}" alt="" class="h-10 w-10 rounded-full object-cover"/>` : "✺";
el.suneBtnTop.innerHTML = a.avatar ? `<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>` : "✺";
el.footer.classList.toggle("hidden", !!a.settings.hide_composer);
await renderSuneHTML();
icons();
@@ -1346,8 +1289,7 @@ var payloadWithSampling = (b) => {
frequency_penalty: num(s.frequency_penalty, null),
repetition_penalty: num(s.repetition_penalty, null),
min_p: num(s.min_p, null),
top_a: num(s.top_a, null),
max_tokens: int(s.max_tokens, null) > 0 ? int(s.max_tokens, null) : null
top_a: num(s.top_a, null)
};
Object.keys(p).forEach((k) => {
const v = p[k];
@@ -1502,15 +1444,6 @@ function showSunePopover(btn, id) {
positionPopover(btn, el.sunePopover);
icons();
}
var hideSunesSyncPopover = () => {
el.sunesSyncPopover.classList.add("hidden");
}, hideSuneSyncPopover = hideSunesSyncPopover;
function showSunesSyncPopover(btn) {
el.sunesSyncPopover.classList.remove("hidden");
positionPopover(btn || el.suneSyncBtn, el.sunesSyncPopover);
icons();
}
var showSuneSyncPopover = showSunesSyncPopover;
$(el.threadList).on("click", async (e) => {
const openBtn = e.target.closest("[data-open-thread]"), menuBtn = e.target.closest("[data-thread-menu]");
if (openBtn) {
@@ -1701,7 +1634,6 @@ $(el.sunePopover).on("click", async (e) => {
SUNE.save();
renderSidebar();
await reflectActiveSune();
markLocalDirty();
};
if (act === "pin") {
s.pinned = !s.pinned;
@@ -1728,20 +1660,13 @@ $(el.sunePopover).on("click", async (e) => {
} else if (act === "export") dl(`sune-${(s.name || "sune").replace(/\W/g, "_")}-${ts()}.sune`, [s]);
hideSunePopover();
});
$(el.sunesSyncUploadBtn).on("click", () => {
hideSunesSyncPopover();
performSuneUpload();
});
$(el.sunesSyncDownloadBtn).on("click", () => {
hideSunesSyncPopover();
performSuneDownload(false);
});
function updateAttachBadge() {
const n = state.attachments.length;
el.attachBadge.textContent = String(n);
el.attachBadge.classList.toggle("hidden", n === 0);
}
$(el.attachBtn).on("click", () => {
if (state.busy) return;
if (state.attachments.length) {
state.attachments = [];
updateAttachBadge();
@@ -1758,18 +1683,6 @@ $(el.fileInput).on("change", async () => {
}
updateAttachBadge();
});
$(el.composer).on("paste", async (e) => {
const files = [...(e.clipboardData || e.originalEvent?.clipboardData)?.files || []];
if (!files.length) return;
e.preventDefault();
state.attachments = [];
el.fileInput.value = "";
for (const f of files) {
const at = await toAttach(f).catch(() => null);
if (at) state.attachments.push(at);
}
updateAttachBadge();
});
$(el.composer).on("submit", async (e) => {
e.preventDefault();
if (state.busy) return;
@@ -1784,9 +1697,6 @@ $(el.composer).on("submit", async (e) => {
text
});
parts.push(...state.attachments);
state.attachments = [];
updateAttachBadge();
el.fileInput.value = "";
const userMsg = {
role: "user",
content: parts.length ? parts : [{
@@ -1800,7 +1710,7 @@ $(el.composer).on("submit", async (e) => {
const title = await generateTitleWithAI(state.messages) || partsToText(state.messages.find((m) => m.role === "user")).replace(/!\[\]\(data:[^\)]+\)/g, "[Image]") || "Untitled";
await THREAD.setTitle(th.id, title);
})();
if (!SUNE.model) return;
if (!SUNE.model) return state.attachments = [], updateAttachBadge();
state.busy = true;
setBtnStop();
const a = SUNE.active, suneMeta = {
@@ -1854,6 +1764,8 @@ $(el.composer).on("submit", async (e) => {
} else if (!done) THREAD.persist(false);
};
await streamChat(onDelta, streamId);
state.attachments = [];
updateAttachBadge();
});
var jars = {
html: null,
@@ -1883,8 +1795,8 @@ function openSettings() {
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_reasoning_effort.value = s.reasoning_effort || "";
el.set_verbosity.value = s.verbosity || "";
el.set_reasoning_effort.value = s.reasoning_effort || "default";
el.set_system_prompt.value = s.system_prompt;
el.set_hide_composer.checked = !!s.hide_composer;
el.set_img_output.checked = !!s.img_output;
@@ -1939,10 +1851,10 @@ $(el.settingsForm).on("submit", async (e) => {
"frequency_penalty",
"repetition_penalty",
"min_p",
"top_a",
"max_tokens"
"top_a"
].forEach((k) => SUNE[k] = el[`set_${k}`].value.trim());
SUNE.reasoning_effort = el.set_reasoning_effort.value || "";
SUNE.verbosity = el.set_verbosity.value || "";
SUNE.reasoning_effort = el.set_reasoning_effort.value || "default";
SUNE.system_prompt = el.set_system_prompt.value.trim();
SUNE.hide_composer = el.set_hide_composer.checked;
SUNE.img_output = el.set_img_output.checked;
@@ -2137,7 +2049,6 @@ async function init() {
renderSidebar();
renderUserUI();
await reflectActiveSune();
if (suR.startsWith("gh://")) checkSuneSyncStatus();
clearChat();
icons();
kbBind();
@@ -2146,10 +2057,6 @@ async function init() {
$(window).on("resize", () => {
hideThreadPopover();
hideSunePopover();
hideSunesSyncPopover();
});
$(document).on("click", (e) => {
if (el.sunesSyncPopover && !el.sunesSyncPopover.classList.contains("hidden") && !el.sunesSyncPopover.contains(e.target) && !el.suneSyncBtn.contains(e.target)) hideSunesSyncPopover();
});
var htmlTabs = {
index: ["htmlTab_index", "htmlEditor"],
@@ -2310,138 +2217,52 @@ $(el.threadSyncBtn).on("click", async () => {
alert("Sync failed: " + e.message);
}
});
var suneSyncBusy = false;
var checkSuneSyncStatus = async () => {
const u = el.suneRepoInput.value.trim();
if (!u.startsWith("gh://")) return setSuneSyncStatus("idle");
if (suneSyncBusy) return;
suneSyncBusy = true;
setSuneSyncStatus("checking");
const info = parseGhUrl(u);
try {
const res = await ghApi(`${info.apiPath}/sunes.json?ref=${info.branch}`);
if (!res) return setSuneSyncStatus("desynced");
res.sha;
let remoteData = null;
if (res.content && res.encoding === "base64") try {
remoteData = JSON.parse(btou(res.content));
} catch {}
if (!remoteData && res.sha) {
const text = await ghGetFileContent(info, "sunes.json");
if (text) try {
remoteData = JSON.parse(text);
} catch {}
}
if (!remoteData) return setSuneSyncStatus("error");
const diff = num(remoteData.updatedAt, 0) - getLocalSunesUpdatedAt();
if (diff > 1e4) await performSuneDownload(true, remoteData, res.sha);
else if (diff < -1e4) setSuneSyncStatus("desynced");
else setSuneSyncStatus("synced");
} catch {
setSuneSyncStatus("error");
} finally {
suneSyncBusy = false;
}
};
var performSuneDownload = async (isAuto = false, prefData = null, prefSha = null) => {
const u = el.suneRepoInput.value.trim();
if (!u.startsWith("gh://")) return;
if (!isAuto && !confirm("Overwrite local sunes with remote version from GitHub?")) return;
setSuneSyncStatus("checking");
const info = parseGhUrl(u);
try {
let data = prefData;
if (!data) {
const res = await ghApi(`${info.apiPath}/sunes.json?ref=${info.branch}`);
if (!res) throw new Error("sunes.json not found");
res.sha;
if (res.content && res.encoding === "base64") try {
data = JSON.parse(btou(res.content));
} catch {}
if (!data) {
const text = await ghGetFileContent(info, "sunes.json");
if (!text) throw new Error("Could not read sunes.json");
data = JSON.parse(text);
}
}
if (!data) throw new Error("Invalid data");
isSyncPulling = true;
try {
if (Array.isArray(data.sunes)) {
sunes = data.sunes.map(makeSune);
su.save(sunes);
const nextActive = data.activeId || (sunes.some((s) => s.id === su.getActiveId()) ? su.getActiveId() : sunes[0]?.id);
if (nextActive) SUNE.setActive(nextActive);
}
if (data.storage && typeof data.storage === "object") {
Object.keys(localStorage).forEach((k) => {
if (isSuneStorageKey(k)) localStorage.removeItem(k);
});
Object.entries(data.storage).forEach(([k, v]) => {
if (isSuneStorageKey(k)) localStorage.setItem(k, v);
});
}
setLocalSunesUpdatedAt(num(data.updatedAt, Date.now()));
} finally {
isSyncPulling = false;
}
renderSidebar();
await reflectActiveSune();
setSuneSyncStatus("synced");
if (!isAuto) alert("Sunes pulled.");
} catch (e) {
setSuneSyncStatus("error");
if (!isAuto) alert("Pull failed: " + e.message);
}
};
var performSuneUpload = async () => {
const u = el.suneRepoInput.value.trim();
if (!u.startsWith("gh://")) return;
if (!confirm("Overwrite remote file on GitHub with local version?")) return;
setSuneSyncStatus("checking");
const info = parseGhUrl(u);
try {
const now = Date.now();
const data = {
version: 1,
updatedAt: now,
sunes: SUNE.list,
activeId: SUNE.id,
storage: {}
};
SUNE.list.forEach((s) => {
const p = `sune_${s.id}_`;
Object.keys(localStorage).forEach((k) => {
if (k.startsWith(p)) data.storage[k] = localStorage.getItem(k);
});
});
const x = await ghApi(`${info.apiPath}/sunes.json?ref=${info.branch}`);
const res = await ghApi(`${info.apiPath}/sunes.json`, "PUT", {
message: "Sync Sunes",
content: utob(JSON.stringify(data, null, 2)),
branch: info.branch,
sha: x?.sha
});
setLocalSunesUpdatedAt(now);
res?.content?.sha;
setSuneSyncStatus("synced");
alert("Sunes pushed.");
} catch (e) {
setSuneSyncStatus("error");
alert("Push failed: " + e.message);
}
};
$(el.suneRepoInput).on("change", () => {
localStorage.setItem("sune_repo_url", el.suneRepoInput.value.trim());
checkSuneSyncStatus();
});
$(el.suneSyncBtn).on("click", (e) => {
e.stopPropagation();
if (!el.suneRepoInput.value.trim().startsWith("gh://")) return;
showSunesSyncPopover(el.suneSyncBtn);
});
$(el.sidebarBtnLeft).on("click", () => {
if (el.suneRepoInput.value.trim().startsWith("gh://")) checkSuneSyncStatus();
$(el.suneSyncBtn).on("click", async () => {
const u = el.suneRepoInput.value.trim();
if (!u.startsWith("gh://")) return;
const mode = confirm("Sync Sunes:\nOK = Upload (Push)\nCancel = Download (Pull)"), info = parseGhUrl(u);
try {
if (mode) {
const data = {
version: 1,
sunes: SUNE.list,
activeId: SUNE.id,
storage: {}
};
SUNE.list.forEach((s) => {
const p = `sune_${s.id}_`;
Object.keys(localStorage).forEach((k) => {
if (k.startsWith(p)) data.storage[k] = localStorage.getItem(k);
});
});
const x = await ghApi(`${info.apiPath}/sunes.json?ref=${info.branch}`);
await ghApi(`${info.apiPath}/sunes.json`, "PUT", {
message: "Sync Sunes",
content: utob(JSON.stringify(data, null, 2)),
branch: info.branch,
sha: x?.sha
});
alert("Sunes pushed.");
} else {
const text = await ghGetFileContent(info, "sunes.json");
if (!text) throw new Error("sunes.json not found");
const data = JSON.parse(text);
if (data.sunes) {
sunes = data.sunes.map(makeSune);
SUNE.save();
if (data.activeId) SUNE.setActive(data.activeId);
}
if (data.storage) Object.entries(data.storage).forEach(([k, v]) => localStorage.setItem(k, v));
renderSidebar();
await reflectActiveSune();
alert("Sunes pulled.");
}
} catch (e) {
alert("Sync failed: " + e.message);
}
});
init();
var accountTabs = {
@@ -2759,14 +2580,6 @@ Object.assign(window, {
showThreadPopover,
hideSunePopover,
showSunePopover,
hideSuneSyncPopover,
showSuneSyncPopover,
hideSunesSyncPopover,
showSunesSyncPopover,
setSuneSyncStatus,
checkSuneSyncStatus,
performSuneDownload,
performSuneUpload,
updateAttachBadge,
toAttach,
ensureJars,

30
dist/index.html vendored
View File

@@ -7,21 +7,21 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.9.0/github-markdown-light.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.12.0/build/styles/github.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-light.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css"/>
<script defer src="https://cdn.jsdelivr.net/npm/cash-dom/dist/cash.min.js"></script>
<script defer src="//unpkg.com/alpinejs"></script>
<script type="module" crossorigin src="/assets/index-CWYcSOgO.js"></script>
<script type="module" crossorigin src="/assets/index-BgxroUTU.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')">
<div class="flex flex-col h-dvh max-h-dvh overflow-hidden">
<header id="topbar" 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-2 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">
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
<button id="suneBtnTop" class="justify-self-center h-10 w-10 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings"></button>
<button id="suneBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings"></button>
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
</div>
</header>
@@ -42,13 +42,13 @@
</footer>
</div>
<div id="sidebarOverlayLeft" class="fixed inset-0 z-40 bg-black/20 hidden" @click="document.getElementById('sidebarLeft').classList.add('-translate-x-full');$el.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full');document.getElementById('sidebarOverlayRight').classList.add('hidden');hideThreadPopover();hideSunePopover();hideSunesSyncPopover()"></div>
<div id="sidebarOverlayLeft" class="fixed inset-0 z-40 bg-black/20 hidden" @click="document.getElementById('sidebarLeft').classList.add('-translate-x-full');$el.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full');document.getElementById('sidebarOverlayRight').classList.add('hidden');hideThreadPopover();hideSunePopover()"></div>
<aside id="sidebarLeft" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
<div class="p-2 border-b flex flex-col gap-2">
<input id="suneRepoInput" type="text" placeholder="gh://owner/sunes" class="w-full h-9 rounded-lg border-0 bg-gray-100 px-3 text-xs font-mono focus:ring-2 focus:ring-black focus:bg-white"/>
<div class="flex items-center justify-between gap-2">
<button id="newSuneBtn" class="px-3 py-1.5 rounded-lg bg-black text-white text-[10px] font-bold uppercase tracking-wider hover:bg-black/90 transition">New Sune</button>
<button id="suneSyncBtn" class="relative px-3 py-1.5 rounded-lg bg-gray-100 text-gray-900 text-[10px] font-bold uppercase tracking-wider hover:bg-gray-200 transition flex items-center gap-1"><i data-lucide="refresh-cw" class="h-3 w-3"></i><span>Sync</span><span id="suneSyncBadge" class="absolute -top-1 -right-1 block h-2.5 w-2.5 rounded-full ring-2 ring-white hidden"></span></button>
<button id="suneSyncBtn" class="px-3 py-1.5 rounded-lg bg-gray-100 text-gray-900 text-[10px] font-bold uppercase tracking-wider hover:bg-gray-200 transition flex items-center gap-1"><i data-lucide="refresh-cw" class="h-3 w-3"></i><span>Sync</span></button>
</div>
</div>
<div id="suneList" class="flex-1 overflow-y-auto divide-y"></div>
@@ -94,10 +94,6 @@
<button data-action="pfp" class="menu-item"><i data-lucide="image" class="h-4 w-4"></i><span>Change pfp</span></button>
<button data-action="export" class="menu-item"><i data-lucide="download" class="h-4 w-4"></i><span>Export sune (.sune)</span></button>
</div>
<div id="sunesSyncPopover" class="menu-card hidden">
<button id="sunesSyncUploadBtn" class="menu-item"><i data-lucide="upload-cloud" class="h-4 w-4"></i><span>Upload to GitHub</span></button>
<button id="sunesSyncDownloadBtn" class="menu-item"><i data-lucide="download-cloud" class="h-4 w-4"></i><span>Download from GitHub</span></button>
</div>
<div id="suneModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div>
<div class="absolute inset-x-0 top-12 mx-auto w-full max-w-md px-4">
@@ -106,7 +102,7 @@
<form id="settingsForm" class="text-sm">
<div class="border-b flex text-xs font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">HTML</button></div>
<div id="panelModel" class="p-4 space-y-4">
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(02)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(01)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
@@ -115,7 +111,7 @@
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(02)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(01)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(01)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Max Tokens (output)</label><input id="set_max_tokens" type="number" min="1" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder=""/></div>
<div><label class="block text-gray-700 font-medium mb-1">Verbosity</label><select id="set_verbosity" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div>
</div>
<div class="flex flex-wrap items-center gap-2 pt-2">
<div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div>
@@ -151,7 +147,6 @@
</div>
<div id="panelPrompt" class="p-4 space-y-4 hidden">
<div><div class="flex items-center justify-between mb-1"><label for="set_system_prompt" class="block text-gray-700 font-medium">System Prompt</label><div class="flex gap-2"><button type="button" id="copySystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteSystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Paste</button></div></div><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Enter a system prompt to guide the sune"></textarea></div>
<p class="text-xs text-gray-500">Use <code>{{model_name}}</code> to insert the model name.</p>
</div>
<div id="panelScript" class="p-1 hidden">
<div class="border-b flex text-xs font-medium"><button type="button" id="htmlTab_index" class="flex-1 py-2 px-3 text-center border-b-2"></button><button type="button" id="htmlTab_extension" class="flex-1 py-2 px-3 text-center border-b-2"></button></div>
@@ -200,8 +195,11 @@
<input id="importAccountSettingsInput" type="file" class="hidden" accept="application/json,.json">
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@15.0.1/dist/browser/markdown-it.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.12.0/build/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.1/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
</body>
</html>

2
dist/sw.js vendored
View File

@@ -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} didnt 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 c=e=>i(e,t),l={module:{uri:t},exports:o,require:c};s[t]=Promise.all(n.map(e=>l[e]||c(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:"f8c767b8fe6e11fa13e704d51b97ffbc"},{url:"assets/index-DUC1RW1F.css",revision:null},{url:"assets/index-CWYcSOgO.js",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} didnt 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:"4dfde2d75701f65ce92ab40f913497db"},{url:"assets/index-DUC1RW1F.css",revision:null},{url:"assets/index-BgxroUTU.js",revision:null},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});

View File

@@ -12,9 +12,12 @@
<load src="/src/parts/sidebars.html" />
<load src="/src/parts/modals.html" />
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@15.0.1/dist/browser/markdown-it.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.12.0/build/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.1/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -1,31 +1,30 @@
[![Download For Android (.apk)](https://img.shields.io/badge/Download-For%20Android%20(.apk)-green?style=for-the-badge&logo=android)](https://github.com/multipleof4/sune/releases/download/v0.23.0/sune-v0.23.0.apk)
<p align="center">
<img src="./public/appstore_content/screenshot1.jpg" alt="Main" width="280" />
</p>
![Main](./public/appstore_content/screenshot1.jpg)
> Each sune is like a module. You can have many. And share them.
<p align="center">
<img src="./public/appstore_content/sune_sidebar.png" alt="Sunes" width="280" />
<img src="./public/appstore_content/screenshot3.jpg" alt="Setting" width="280" />
</p>
![Sunes](./public/appstore_content/sune_sidebar.png)
![Setting](./public/appstore_content/screenshot3.jpg)
💠 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.
<p align="center">
<img src="./public/appstore_content/screenshot4.jpg" alt="Scripting" width="280" />
<img src="./public/appstore_content/screenshot_miku.png" alt="Miku" width="280" />
</p>
![Scripting](./public/appstore_content/screenshot4.jpg)
> There is a marketplace and LaTeX support out of the box.
> Image support.
<p align="center">
<img src="./public/appstore_content/screenshot_marketplace.jpg" alt="Marketplace" width="280" />
<img src="./public/appstore_content/latex.png" alt="LaTeX" width="280" />
</p>
![Miku](./public/appstore_content/screenshot_miku.png)
> There is a marketplace.
![Marketplace](./public/appstore_content/screenshot_marketplace.jpg)
> LaTeX support out of the box.
![LaTeX](./public/appstore_content/latex.png)
---
@@ -33,9 +32,7 @@
Never lose a conversation again. Sune can sync all your threads to a GitHub repo.
<p align="center">
<img src="./public/appstore_content/sync.png" alt="Sync" width="320" />
</p>
![Sync](./public/appstore_content/sync.png)
### Setup for Syncing Chats

View File

@@ -3,7 +3,7 @@ export const el = window.el = Object.fromEntries(
'topbar','chat','messages','composer','input','sendBtn','suneBtnTop','suneModal','suneURL',
'settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript',
'panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k',
'set_frequency_penalty','set_repetition_penalty','set_min_p','set_top_a','set_max_tokens',
'set_frequency_penalty','set_repetition_penalty','set_min_p','set_top_a','set_verbosity',
'set_reasoning_effort','set_system_prompt','set_hide_composer','set_include_thoughts',
'set_img_output','set_aspect_ratio','set_image_size','aspectRatioContainer',
'set_ignore_master_prompt','deleteSuneBtn','sidebarLeft','sidebarOverlayLeft','sidebarBtnLeft',
@@ -19,7 +19,6 @@ export const el = window.el = Object.fromEntries(
'importAccountSettings','exportAccountSettings',
'importAccountSettingsInput','accountTabUser','accountPanelUser','set_user_name',
'userAvatarPreview','setUserAvatarBtn','userAvatarInput','threadRepoInput','threadBackBtn',
'threadFolderBtn','threadSyncBtn','suneRepoInput','suneSyncBtn',
'suneSyncBadge','sunesSyncPopover','sunesSyncUploadBtn','sunesSyncDownloadBtn'
'threadFolderBtn','threadSyncBtn','suneRepoInput','suneSyncBtn'
].map(id => [id, document.getElementById(id)])
);

View File

@@ -11,11 +11,6 @@ export function kbUpdate() {
}
export function kbBind() {
el.input.addEventListener('keydown', e => {
if (e.key !== 'Enter' || e.shiftKey || e.isComposing || e.keyCode === 229 || e.repeat || !matchMedia('(hover: hover) and (pointer: fine)').matches) return;
e.preventDefault();
el.composer.requestSubmit();
});
if (window.visualViewport) {
['resize', 'scroll'].forEach(ev => window.visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true }));
}

View File

@@ -17,36 +17,10 @@ const DEFAULT_MODEL='openrouter/free'
const icons=()=>window.lucide&&lucide.createIcons()
const haptic=()=>/android/i.test(navigator.userAgent)&&navigator.vibrate?.(1)
const SYSTEM_KEYS=new Set(['sunes_v1','active_sune_id','thread_repo_url','sune_repo_url','sunes_updated_at','user_name','user_avatar','provider','openrouter_api_key','openai_api_key','google_api_key','claude_api_key','cloudflare_api_key','master_prompt','title_model','gh_token','custom_key_1'])
const SYSTEM_KEYS=new Set(['sunes_v1','active_sune_id','thread_repo_url','sune_repo_url','user_name','user_avatar','provider','openrouter_api_key','openai_api_key','google_api_key','claude_api_key','cloudflare_api_key','master_prompt','title_model','gh_token','custom_key_1'])
const gcStorage=()=>{try{const alive=new Set(sunes.map(s=>s.id));Object.keys(localStorage).forEach(k=>{if(SYSTEM_KEYS.has(k)||/^(t_|rem_t_|rem_index_|localforage|threads_)/.test(k))return;const m=k.match(/^sune_([^_]+)_/);if(m&&alive.has(m[1]))return;localStorage.removeItem(k)})}catch{}}
const cleanSuneStorage=id=>{if(!id)return;const p=`sune_${id}_`;Object.keys(localStorage).forEach(k=>{if(k.startsWith(p))localStorage.removeItem(k)})}
let isSyncPulling=false,suneSyncStatus='idle',remoteSha=null;
const getLocalSunesUpdatedAt=()=>num(localStorage.getItem('sunes_updated_at'),0);
const setLocalSunesUpdatedAt=(ts=Date.now())=>localStorage.setItem('sunes_updated_at',ts);
const markLocalDirty=()=>{if(isSyncPulling)return;setLocalSunesUpdatedAt();setSuneSyncStatus('desynced')};
const isSuneStorageKey=k=>!SYSTEM_KEYS.has(k)&&/^sune_[^_]+_/.test(k);
const _origSetItem=localStorage.setItem.bind(localStorage),_origRemoveItem=localStorage.removeItem.bind(localStorage);
localStorage.setItem=(k,v)=>{_origSetItem(k,v);if(!isSyncPulling&&isSuneStorageKey(k))markLocalDirty()};
localStorage.removeItem=k=>{_origRemoveItem(k);if(!isSyncPulling&&isSuneStorageKey(k))markLocalDirty()};
function setSuneSyncStatus(status){
suneSyncStatus=status;
const b=el.suneSyncBadge;
if(!b)return;
const u=(el.suneRepoInput?.value||'').trim();
if(!u.startsWith('gh://')){b.className='hidden';return}
b.className='absolute -top-1 -right-1 block h-2.5 w-2.5 rounded-full ring-2 ring-white';
switch(status){
case 'checking':b.classList.add('bg-blue-500','animate-pulse');break;
case 'synced':b.classList.add('bg-green-500');break;
case 'desynced':b.classList.add('bg-amber-500');break;
case 'error':b.classList.add('bg-red-500');break;
default:b.classList.add('bg-gray-400');
}
el.suneSyncBtn?.querySelector('svg')?.classList.toggle('animate-spin',status==='checking');
}
const suneStorage={
get(k,def=null){const id=SUNE.id;if(!id)return def;const v=localStorage.getItem(`sune_${id}_${k}`);if(v===null)return def;try{return JSON.parse(v)}catch{return v}},
set(k,v){const id=SUNE.id;if(id)localStorage.setItem(`sune_${id}_${k}`,JSON.stringify(v))},
@@ -57,14 +31,14 @@ const suneStorage={
}
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:'',top_p:'',top_k:'',frequency_penalty:'',repetition_penalty:'',min_p:'',top_a:'',max_tokens:'',reasoning_effort:'',system_prompt:'',html:'',extension_html:"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",hide_composer:false,include_thoughts:false,img_output:false,aspect_ratio:'1:1',image_size:'1K',ignore_master_prompt:false}
const defaultSettings={model:DEFAULT_MODEL,temperature:'',top_p:'',top_k:'',frequency_penalty:'',repetition_penalty:'',min_p:'',top_a:'',verbosity:'',reasoning_effort:'default',system_prompt:'',html:'',extension_html:"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",hide_composer:false,include_thoughts:false,img_output:false,aspect_ratio:'1:1',image_size:'1K',ignore_master_prompt:false}
const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',url:p.url||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{})})
let sunes=(su.load()||[]).map(makeSune)
const SUNE=window.SUNE=new Proxy({storage:suneStorage,get list(){return sunes},get id(){return su.getActiveId()},get active(){return sunes.find(a=>a.id===su.getActiveId())||sunes[0]},get:id=>sunes.find(s=>s.id===id),setActive:id=>su.setActiveId(id||''),create(p={}){const s=makeSune(p);sunes.unshift(s);su.save(sunes);markLocalDirty();return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);cleanSuneStorage(id);gcStorage();markLocalDirty();if(sunes.length===0){const def=this.create({name:'Default'});this.setActive(def.id)}else if(curId===id)this.setActive(sunes[0].id)},save:()=>su.save(sunes)},{get(t,p){if(p==='fetchDotSune')return async g=>{try{const u=g.startsWith('http')?g:(()=>{const[a,b]=g.split('@'),[c,d]=a.split('/'),[e,...f]=b.split('/');return`https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join('/')}`})(),j=await(await fetch(u)).json(),l=sunes.length;sunes.unshift(...(Array.isArray(j)?j:j?.sunes||[]).filter(s=>s?.id&&!t.get(s.id)).map(s=>makeSune(s)));sunes.length>l&&t.save()}catch{}};if(p==='attach')return async files=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser('(attachments)');addMessage({role:'assistant',content:clean,...activeMeta()});await THREAD.persist()};if(p==='log')return async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await THREAD.persist()};if(p==='lastReply')return [...state.messages].reverse().find(m=>m.role==='assistant');if(p==='infer')return async()=>{if(state.busy||!SUNE.model||state.abortRequested){state.abortRequested=false;return};await ensureThreadOnFirstUser('Sune Inference');const th=THREAD.active;if(th&&!th.title)(async()=>THREAD.setTitle(th.id,await generateTitleWithAI(state.messages)||'Sune Inference'))();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:null,bubble:null,meta:null,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;assistantMsg.content[0].text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId)};if(p==='getByName')return n=>sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(p==='handoff')return async n=>{await new Promise(r=>setTimeout(r,4000));const s=sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(!s)return;SUNE.setActive(s.id);renderSidebar();await reflectActiveSune();await SUNE.infer()};if(p in t)return t[p];const a=t.active;if(!a)return;if(p in a.settings)return a.settings[p];if(p in a)return a[p]},set(t,p,v){const a=t.active;if(!a)return false;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return false;const isTopLevel=/^(name|avatar|url|pinned)$/.test(p),target=isTopLevel?sunes[i]:sunes[i].settings;let value=v;if(!isTopLevel){if(p==='system_prompt')value=v||''}if(target[p]!==value){target[p]=value;sunes[i].updatedAt=Date.now();su.save(sunes);markLocalDirty()}return true}})
const SUNE=window.SUNE=new Proxy({storage:suneStorage,get list(){return sunes},get id(){return su.getActiveId()},get active(){return sunes.find(a=>a.id===su.getActiveId())||sunes[0]},get:id=>sunes.find(s=>s.id===id),setActive:id=>su.setActiveId(id||''),create(p={}){const s=makeSune(p);sunes.unshift(s);su.save(sunes);return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);cleanSuneStorage(id);gcStorage();if(sunes.length===0){const def=this.create({name:'Default'});this.setActive(def.id)}else if(curId===id)this.setActive(sunes[0].id)},save:()=>su.save(sunes)},{get(t,p){if(p==='fetchDotSune')return async g=>{try{const u=g.startsWith('http')?g:(()=>{const[a,b]=g.split('@'),[c,d]=a.split('/'),[e,...f]=b.split('/');return`https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join('/')}`})(),j=await(await fetch(u)).json(),l=sunes.length;sunes.unshift(...(Array.isArray(j)?j:j?.sunes||[]).filter(s=>s?.id&&!t.get(s.id)).map(s=>makeSune(s)));sunes.length>l&&t.save()}catch{}};if(p==='attach')return async files=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser('(attachments)');addMessage({role:'assistant',content:clean,...activeMeta()});await THREAD.persist()};if(p==='log')return async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await THREAD.persist()};if(p==='lastReply')return [...state.messages].reverse().find(m=>m.role==='assistant');if(p==='infer')return async()=>{if(state.busy||!SUNE.model||state.abortRequested){state.abortRequested=false;return};await ensureThreadOnFirstUser('Sune Inference');const th=THREAD.active;if(th&&!th.title)(async()=>THREAD.setTitle(th.id,await generateTitleWithAI(state.messages)||'Sune Inference'))();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:null,bubble:null,meta:null,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;assistantMsg.content[0].text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId)};if(p==='getByName')return n=>sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(p==='handoff')return async n=>{await new Promise(r=>setTimeout(r,4000));const s=sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(!s)return;SUNE.setActive(s.id);renderSidebar();await reflectActiveSune();await SUNE.infer()};if(p in t)return t[p];const a=t.active;if(!a)return;if(p in a.settings)return a.settings[p];if(p in a)return a[p]},set(t,p,v){const a=t.active;if(!a)return false;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return false;const isTopLevel=/^(name|avatar|url|pinned)$/.test(p),target=isTopLevel?sunes[i]:sunes[i].settings;let value=v;if(!isTopLevel){if(p==='system_prompt')value=v||''}if(target[p]!==value){target[p]=value;sunes[i].updatedAt=Date.now();su.save(sunes)}return true}})
if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)}
const state=window.state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:false}}
const getModelShort=m=>{const mm=m||SUNE.model||'';return mm.includes('/')?mm.split('/').pop():mm}
const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.title=`Settings — ${a.name}`;el.suneBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-10 w-10 rounded-full object-cover"/>`:'✺';el.footer.classList.toggle('hidden',!!a.settings.hide_composer);await renderSuneHTML();icons()}
const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.title=`Settings — ${a.name}`;el.suneBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺';el.footer.classList.toggle('hidden',!!a.settings.hide_composer);await renderSuneHTML();icons()}
const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===SUNE.id?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.id}" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center" title="More"><i data-lucide="more-horizontal" class="h-4 w-4"></i></button></div>`
const renderSidebar=window.renderSidebar=()=>{const list=[...SUNE.list].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()}
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='👤'}}
@@ -74,7 +48,7 @@ function msgRow(m){const $row=_createMessageRow(m);$(el.messages).append($row);q
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))
const clearChat=()=>{el.suneHtml.dispatchEvent(new CustomEvent('sune:unmount'));state.messages=[];el.messages.innerHTML='';state.attachments=[];updateAttachBadge();el.fileInput.value=''}
const payloadWithSampling=b=>{const o=Object.assign({},b),s=SUNE,p={temperature:num(s.temperature,null),top_p:num(s.top_p,null),top_k:int(s.top_k,null),frequency_penalty:num(s.frequency_penalty,null),repetition_penalty:num(s.repetition_penalty,null),min_p:num(s.min_p,null),top_a:num(s.top_a,null),max_tokens:int(s.max_tokens,null)>0?int(s.max_tokens,null):null};Object.keys(p).forEach(k=>{const v=p[k];if(v!==null)o[k]=v});return o}
const payloadWithSampling=b=>{const o=Object.assign({},b),s=SUNE,p={temperature:num(s.temperature,null),top_p:num(s.top_p,null),top_k:int(s.top_k,null),frequency_penalty:num(s.frequency_penalty,null),repetition_penalty:num(s.repetition_penalty,null),min_p:num(s.min_p,null),top_a:num(s.top_a,null)};Object.keys(p).forEach(k=>{const v=p[k];if(v!==null)o[k]=v});return o}
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.();state.busy=false;setBtnSend()}}
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
function localDemoReply(){return 'Tip: open the sidebar → User to set your API key.'}
@@ -97,9 +71,6 @@ let menuThreadId=null;const hideThreadPopover=()=>{el.threadPopover.classList.ad
function showThreadPopover(btn,id){menuThreadId=id;el.threadPopover.classList.remove('hidden');positionPopover(btn,el.threadPopover);icons()}
let menuSuneId=null;const hideSunePopover=()=>{el.sunePopover.classList.add('hidden');menuSuneId=null}
function showSunePopover(btn,id){menuSuneId=id;el.sunePopover.classList.remove('hidden');positionPopover(btn,el.sunePopover);icons()}
const hideSunesSyncPopover=()=>{el.sunesSyncPopover.classList.add('hidden')},hideSuneSyncPopover=hideSunesSyncPopover;
function showSunesSyncPopover(btn){el.sunesSyncPopover.classList.remove('hidden');positionPopover(btn||el.suneSyncBtn,el.sunesSyncPopover);icons()}
const showSuneSyncPopover=showSunesSyncPopover;
$(el.threadList).on('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'),type=openBtn.getAttribute('data-type');if(type==='file'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);window.open(`https://github.com/${info.owner}/${info.repo}/blob/${info.branch}/${id}`,'_blank')}return}if(type==='folder'){const u=el.threadRepoInput.value.trim();el.threadRepoInput.value=u+(u.endsWith('/')?'':'/')+id;el.threadRepoInput.dispatchEvent(new Event('change'));return}if(id!==state.currentThreadId&&state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=false;state.controller=null}const th=THREAD.get(id);if(!th)return;if(id===state.currentThreadId){el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();return}state.currentThreadId=id;clearChat();const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';let msgs=await localforage.getItem(prefix+id);if((!msgs||!Array.isArray(msgs)||!msgs.length)&&u.startsWith('gh://')){try{const info=parseGhUrl(u),fileName=serializeThreadName(th),text=await ghGetFileContent(info,fileName);if(text){try{msgs=JSON.parse(text);await localforage.setItem(prefix+id,msgs);th.status='synced';await THREAD.save()}catch(pe){console.error('[Sune] Thread JSON parse failed for',fileName,'len',text.length,pe)}}else{console.warn('[Sune] Remote thread returned no content:',fileName)}}catch(e){console.error('[Sune] Remote fetch failed',e)}}state.messages=Array.isArray(msgs)?[...msgs]:[];for(const m of state.messages){const b=msgRow(m);b.dataset.mid=m.id||'';renderMarkdown(b,partsToText(m))}await renderSuneHTML();syncWhileBusy();queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}));el.sidebarRight.classList.add('translate-x-full');el.sidebarOverlayRight.classList.add('hidden');hideThreadPopover();return}if(menuBtn){e.stopPropagation();showThreadPopover(menuBtn,menuBtn.getAttribute('[data-thread-menu]')?menuBtn.getAttribute('[data-thread-menu]'):menuBtn.getAttribute('data-thread-menu'))}})
$(el.threadList).on('scroll',()=>{
if(isAddingThreads||el.threadList.scrollTop+el.threadList.clientHeight<el.threadList.scrollHeight-200)return;
@@ -113,24 +84,13 @@ $(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);if(await copyToClipboard(`${info.owner}/${info.repo}@${info.branch}/${th.id}`))alert('Path copied.')}}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();markLocalDirty()};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()})
$(el.sunesSyncUploadBtn).on('click',()=>{hideSunesSyncPopover();performSuneUpload()});
$(el.sunesSyncDownloadBtn).on('click',()=>{hideSunesSyncPopover();performSuneDownload(false)});
$(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)}
$(el.attachBtn).on('click',()=>{if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.value=''};el.fileInput.click()})
$(el.attachBtn).on('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.value=''};el.fileInput.click()})
$(el.fileInput).on('change',async()=>{const files=[...(el.fileInput.files||[])];if(!files.length)return;for(const f of files){const at=await toAttach(f).catch(()=>null);if(at)state.attachments.push(at)}updateAttachBadge()})
$(el.composer).on('paste',async e=>{
const cbd=e.clipboardData||e.originalEvent?.clipboardData,files=[...(cbd?.files||[])];
if(!files.length)return;
e.preventDefault();
state.attachments=[];
el.fileInput.value='';
for(const f of files){const at=await toAttach(f).catch(()=>null);if(at)state.attachments.push(at)}
updateAttachBadge();
})
$(el.composer).on('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return SUNE.infer();await ensureThreadOnFirstUser(text||'(attachments)');const th=THREAD.active,shouldGenTitle=th&&!th.title;el.input.value='';const parts=[];if(text)parts.push({type:'text',text});parts.push(...state.attachments);state.attachments=[];updateAttachBadge();el.fileInput.value='';const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};addMessage(userMsg);el.composer.dispatchEvent(new CustomEvent('user:send',{detail:{message:userMsg}}));if(shouldGenTitle)(async()=>{const title=await generateTitleWithAI(state.messages)||partsToText(state.messages.find(m=>m.role==='user')).replace(/!\[\]\(data:[^\)]+\)/g,'[Image]')||'Untitled';await THREAD.setTitle(th.id,title)})();if(!SUNE.model)return;state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;assistantMsg.content[0].text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId)})
$(el.composer).on('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return SUNE.infer();await ensureThreadOnFirstUser(text||'(attachments)');const th=THREAD.active,shouldGenTitle=th&&!th.title;el.input.value='';const parts=[];if(text)parts.push({type:'text',text});parts.push(...state.attachments);const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};addMessage(userMsg);el.composer.dispatchEvent(new CustomEvent('user:send',{detail:{message:userMsg}}));if(shouldGenTitle)(async()=>{const title=await generateTitleWithAI(state.messages)||partsToText(state.messages.find(m=>m.role==='user')).replace(/!\[\]\(data:[^\)]+\)/g,'[Image]')||'Untitled';await THREAD.setTitle(th.id,title)})();if(!SUNE.model)return state.attachments=[],updateAttachBadge();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;assistantMsg.content[0].text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId);state.attachments=[];updateAttachBadge()})
let jars={html:null,extension:null};
const ensureJars=async()=>{
if(jars.html&&jars.extension)return jars;
@@ -144,7 +104,7 @@ const ensureJars=async()=>{
return jars;
}
let openedHTML=false
function openSettings(){const a=SUNE.active,s=a.settings;openedHTML=false;el.suneURL.value=a.url||'';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_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_reasoning_effort.value=s.reasoning_effort||'';el.set_system_prompt.value=s.system_prompt;el.set_hide_composer.checked=!!s.hide_composer;el.set_img_output.checked=!!s.img_output;el.set_aspect_ratio.value=s.aspect_ratio||'1:1';el.set_image_size.value=s.image_size||'1K';el.aspectRatioContainer.classList.toggle('hidden',!s.img_output);el.set_include_thoughts.checked=!!s.include_thoughts;el.set_ignore_master_prompt.checked=!!s.ignore_master_prompt;showTab('Model');el.suneModal.classList.remove('hidden')}
function openSettings(){const a=SUNE.active,s=a.settings;openedHTML=false;el.suneURL.value=a.url||'';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_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_verbosity.value=s.verbosity||'';el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;el.set_hide_composer.checked=!!s.hide_composer;el.set_img_output.checked=!!s.img_output;el.set_aspect_ratio.value=s.aspect_ratio||'1:1';el.set_image_size.value=s.image_size||'1K';el.aspectRatioContainer.classList.toggle('hidden',!s.img_output);el.set_include_thoughts.checked=!!s.include_thoughts;el.set_ignore_master_prompt.checked=!!s.ignore_master_prompt;showTab('Model');el.suneModal.classList.remove('hidden')}
const closeSettings=()=>{el.suneModal.classList.add('hidden')}
const tabs={Model:['tabModel','panelModel'],Prompt:['tabPrompt','panelPrompt'],Script:['tabScript','panelScript']}
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'){openedHTML=true;showHtmlTab('index');ensureJars().then(({html,extension})=>{const s=SUNE.settings;html.updateCode(s.html||'');extension.updateCode(s.extension_html||'')})}}
@@ -155,7 +115,7 @@ $(el.tabModel).on('click',()=>showTab('Model'))
$(el.tabPrompt).on('click',()=>showTab('Prompt'))
$(el.tabScript).on('click',()=>showTab('Script'))
$(el.set_img_output).on('change',e=>el.aspectRatioContainer.classList.toggle('hidden',!e.target.checked))
$(el.settingsForm).on('submit',async e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a','max_tokens'].forEach(k=>SUNE[k]=el[`set_${k}`].value.trim());SUNE.reasoning_effort=(el.set_reasoning_effort.value||'');SUNE.system_prompt=el.set_system_prompt.value.trim();SUNE.hide_composer=el.set_hide_composer.checked;SUNE.img_output=el.set_img_output.checked;SUNE.aspect_ratio=el.set_aspect_ratio.value;SUNE.image_size=el.set_image_size.value;SUNE.include_thoughts=el.set_include_thoughts.checked;SUNE.ignore_master_prompt=el.set_ignore_master_prompt.checked;if(openedHTML){SUNE.html=jars.html.toString();SUNE.extension_html=jars.extension.toString()}closeSettings();await reflectActiveSune()})
$(el.settingsForm).on('submit',async e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a'].forEach(k=>SUNE[k]=el[`set_${k}`].value.trim());SUNE.verbosity=(el.set_verbosity.value||'');SUNE.reasoning_effort=(el.set_reasoning_effort.value||'default');SUNE.system_prompt=el.set_system_prompt.value.trim();SUNE.hide_composer=el.set_hide_composer.checked;SUNE.img_output=el.set_img_output.checked;SUNE.aspect_ratio=el.set_aspect_ratio.value;SUNE.image_size=el.set_image_size.value;SUNE.include_thoughts=el.set_include_thoughts.checked;SUNE.ignore_master_prompt=el.set_ignore_master_prompt.checked;if(openedHTML){SUNE.html=jars.html.toString();SUNE.extension_html=jars.extension.toString()}closeSettings();await reflectActiveSune()})
$(el.deleteSuneBtn).on('click',async()=>{const activeId=SUNE.id,name=SUNE.name||'this sune';if(!confirm(`Delete "${name}"?`))return;SUNE.delete(activeId);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
$(el.newSuneBtn).on('click',async()=>{const name=prompt('Name your sune:');if(!name)return;const sune=SUNE.create({name:name.trim()});SUNE.setActive(sune.id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')})
let importMode=null
@@ -195,9 +155,8 @@ USER.logMany = async msgs => {
await THREAD.persist();
};
async function init(){gcStorage();const u=localStorage.getItem('thread_repo_url')||'',suR=localStorage.getItem('sune_repo_url')||'';el.threadRepoInput.value=u;el.suneRepoInput.value=suR;el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);await THREAD.load();await renderThreads();await Promise.allSettled(STICKY_SUNES.map(s=>SUNE.fetchDotSune(s)));renderSidebar();renderUserUI();await reflectActiveSune();if(suR.startsWith('gh://'))checkSuneSyncStatus();clearChat();icons();kbBind();kbUpdate()}
$(window).on('resize',()=>{hideThreadPopover();hideSunePopover();hideSunesSyncPopover()})
$(document).on('click',e=>{if(el.sunesSyncPopover&&!el.sunesSyncPopover.classList.contains('hidden')&&!el.sunesSyncPopover.contains(e.target)&&!el.suneSyncBtn.contains(e.target))hideSunesSyncPopover()});
async function init(){gcStorage();const u=localStorage.getItem('thread_repo_url')||'',suR=localStorage.getItem('sune_repo_url')||'';el.threadRepoInput.value=u;el.suneRepoInput.value=suR;el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);await THREAD.load();await renderThreads();await Promise.allSettled(STICKY_SUNES.map(s=>SUNE.fetchDotSune(s)));renderSidebar();renderUserUI();await reflectActiveSune();clearChat();icons();kbBind();kbUpdate()}
$(window).on('resize',()=>{hideThreadPopover();hideSunePopover()})
const htmlTabs={index:['htmlTab_index','htmlEditor'],extension:['htmlTab_extension','extensionHtmlEditor']};function showHtmlTab(key){Object.entries(htmlTabs).forEach(([k,[tb,pn]])=>{const a=k===key;el[tb].classList.toggle('border-black',a);el[tb].classList.toggle('border-transparent',!a);el[tb].classList.toggle('hover:border-gray-300',!a);el[pn].classList.toggle('hidden',!a)})}
el.htmlTab_index.textContent='index.html';el.htmlTab_extension.textContent='extension.html';
el.htmlTab_index.onclick=()=>showHtmlTab('index');el.htmlTab_extension.onclick=()=>showHtmlTab('extension');
@@ -206,127 +165,8 @@ $(el.threadRepoInput).on('change',async()=>{const u=el.threadRepoInput.value.tri
$(el.threadBackBtn).on('click',()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const p=u.split('/');if(p.length>3){p.pop();el.threadRepoInput.value=p.join('/');el.threadRepoInput.dispatchEvent(new Event('change'))}});
$(el.threadFolderBtn).on('click',async()=>{const n=prompt('Folder name:');if(!n)return;THREAD.list.unshift({id:n.trim(),title:n.trim(),type:'folder',updatedAt:Date.now()});await THREAD.save();await renderThreads()});
$(el.threadSyncBtn).on('click',async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const mode=confirm('Sync Threads:\nOK = Upload (Push)\nCancel = Download (Pull)');const info=parseGhUrl(u);try{if(mode){const remoteItems=await ghApi(`${info.apiPath}?ref=${info.branch}`)||[],remoteMap={};remoteItems.forEach(i=>{const d=deserializeThreadName(i.name);if(d)remoteMap[d.id]={name:i.name,sha:i.sha}});const toRemove=[];for(const t of THREAD.list){if(t.status==='deleted'){if(remoteMap[t.id]){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Delete thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch});await localforage.removeItem('rem_t_'+t.id)}toRemove.push(t.id);continue}if(t.type!=='thread')continue;if(t.status==='modified'||t.status==='new'){const newName=serializeThreadName(t);let msgs=await localforage.getItem('rem_t_'+t.id);if((!msgs||!Array.isArray(msgs))&&remoteMap[t.id]){const text=await ghGetFileContent(info,remoteMap[t.id].name);if(text){try{msgs=JSON.parse(text);await localforage.setItem('rem_t_'+t.id,msgs)}catch(e){console.error(e)}}}if(remoteMap[t.id]&&remoteMap[t.id].name!==newName){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Rename thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch})}const x=await ghApi(`${info.apiPath}/${newName}?ref=${info.branch}`);await ghApi(`${info.apiPath}/${newName}`,'PUT',{message:`Sync thread ${t.id}`,content:utob(JSON.stringify(msgs||[],null,2)),branch:info.branch,sha:x?.sha});t.status='synced'}}THREAD.list=THREAD.list.filter(x=>!toRemove.includes(x.id));await THREAD.save();alert('Pushed to GitHub.')}else{await pullThreads();alert('Pulled from GitHub.')}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}});
let suneSyncBusy=false;
const checkSuneSyncStatus=async()=>{
const u=el.suneRepoInput.value.trim();
if(!u.startsWith('gh://'))return setSuneSyncStatus('idle');
if(suneSyncBusy)return;
suneSyncBusy=true;
setSuneSyncStatus('checking');
const info=parseGhUrl(u);
try{
const res=await ghApi(`${info.apiPath}/sunes.json?ref=${info.branch}`);
if(!res){remoteSha=null;return setSuneSyncStatus('desynced')}
remoteSha=res.sha;
let remoteData=null;
if(res.content&&res.encoding==='base64'){try{remoteData=JSON.parse(btou(res.content))}catch{}}
if(!remoteData&&res.sha){
const text=await ghGetFileContent(info,'sunes.json');
if(text){try{remoteData=JSON.parse(text)}catch{}}
}
if(!remoteData)return setSuneSyncStatus('error');
const remoteUp=num(remoteData.updatedAt,0),localUp=getLocalSunesUpdatedAt(),diff=remoteUp-localUp;
if(diff>10000)await performSuneDownload(true,remoteData,res.sha);
else if(diff<-10000)setSuneSyncStatus('desynced');
else setSuneSyncStatus('synced');
}catch{
setSuneSyncStatus('error');
}finally{
suneSyncBusy=false;
}
};
const performSuneDownload=async(isAuto=false,prefData=null,prefSha=null)=>{
const u=el.suneRepoInput.value.trim();
if(!u.startsWith('gh://'))return;
if(!isAuto&&!confirm('Overwrite local sunes with remote version from GitHub?'))return;
setSuneSyncStatus('checking');
const info=parseGhUrl(u);
try{
let data=prefData,sha=prefSha;
if(!data){
const res=await ghApi(`${info.apiPath}/sunes.json?ref=${info.branch}`);
if(!res)throw new Error('sunes.json not found');
sha=res.sha;
if(res.content&&res.encoding==='base64'){try{data=JSON.parse(btou(res.content))}catch{}}
if(!data){
const text=await ghGetFileContent(info,'sunes.json');
if(!text)throw new Error('Could not read sunes.json');
data=JSON.parse(text);
}
}
if(!data)throw new Error('Invalid data');
isSyncPulling=true;
try{
if(Array.isArray(data.sunes)){
sunes=data.sunes.map(makeSune);
su.save(sunes);
const nextActive=data.activeId||(sunes.some(s=>s.id===su.getActiveId())?su.getActiveId():sunes[0]?.id);
if(nextActive)SUNE.setActive(nextActive);
}
if(data.storage&&typeof data.storage==='object'){
Object.keys(localStorage).forEach(k=>{if(isSuneStorageKey(k))localStorage.removeItem(k)});
Object.entries(data.storage).forEach(([k,v])=>{if(isSuneStorageKey(k))localStorage.setItem(k,v)});
}
setLocalSunesUpdatedAt(num(data.updatedAt,Date.now()));
remoteSha=sha;
}finally{
isSyncPulling=false;
}
renderSidebar();
await reflectActiveSune();
setSuneSyncStatus('synced');
if(!isAuto)alert('Sunes pulled.');
}catch(e){
setSuneSyncStatus('error');
if(!isAuto)alert('Pull failed: '+e.message);
}
};
const performSuneUpload=async()=>{
const u=el.suneRepoInput.value.trim();
if(!u.startsWith('gh://'))return;
if(!confirm('Overwrite remote file on GitHub with local version?'))return;
setSuneSyncStatus('checking');
const info=parseGhUrl(u);
try{
const now=Date.now();
const data={version:1,updatedAt:now,sunes:SUNE.list,activeId:SUNE.id,storage:{}};
SUNE.list.forEach(s=>{
const p=`sune_${s.id}_`;
Object.keys(localStorage).forEach(k=>{if(k.startsWith(p))data.storage[k]=localStorage.getItem(k)});
});
const x=await ghApi(`${info.apiPath}/sunes.json?ref=${info.branch}`);
const res=await ghApi(`${info.apiPath}/sunes.json`,'PUT',{
message:'Sync Sunes',
content:utob(JSON.stringify(data,null,2)),
branch:info.branch,
sha:x?.sha
});
setLocalSunesUpdatedAt(now);
remoteSha=res?.content?.sha||null;
setSuneSyncStatus('synced');
alert('Sunes pushed.');
}catch(e){
setSuneSyncStatus('error');
alert('Push failed: '+e.message);
}
};
$(el.suneRepoInput).on('change',()=>{
localStorage.setItem('sune_repo_url',el.suneRepoInput.value.trim());
checkSuneSyncStatus();
});
$(el.suneSyncBtn).on('click',e=>{
e.stopPropagation();
const u=el.suneRepoInput.value.trim();
if(!u.startsWith('gh://'))return;
showSunesSyncPopover(el.suneSyncBtn);
});
$(el.sidebarBtnLeft).on('click',()=>{
if(el.suneRepoInput.value.trim().startsWith('gh://'))checkSuneSyncStatus();
});
$(el.suneRepoInput).on('change',()=>{localStorage.setItem('sune_repo_url',el.suneRepoInput.value.trim())});
$(el.suneSyncBtn).on('click',async()=>{const u=el.suneRepoInput.value.trim();if(!u.startsWith('gh://'))return;const mode=confirm('Sync Sunes:\nOK = Upload (Push)\nCancel = Download (Pull)'),info=parseGhUrl(u);try{if(mode){const data={version:1,sunes:SUNE.list,activeId:SUNE.id,storage:{}};SUNE.list.forEach(s=>{const p=`sune_${s.id}_`;Object.keys(localStorage).forEach(k=>{if(k.startsWith(p))data.storage[k]=localStorage.getItem(k)})});const x=await ghApi(`${info.apiPath}/sunes.json?ref=${info.branch}`);await ghApi(`${info.apiPath}/sunes.json`,'PUT',{message:'Sync Sunes',content:utob(JSON.stringify(data,null,2)),branch:info.branch,sha:x?.sha});alert('Sunes pushed.')}else{const text=await ghGetFileContent(info,'sunes.json');if(!text)throw new Error('sunes.json not found');const data=JSON.parse(text);if(data.sunes){sunes=data.sunes.map(makeSune);SUNE.save();if(data.activeId)SUNE.setActive(data.activeId)}if(data.storage){Object.entries(data.storage).forEach(([k,v])=>localStorage.setItem(k,v))}renderSidebar();await reflectActiveSune();alert('Sunes pulled.')}}catch(e){alert('Sync failed: '+e.message)}});
init()
const accountTabs={General:['accountTabGeneral','accountPanelGeneral'],API:['accountTabAPI','accountPanelAPI'],User:['accountTabUser','accountPanelUser']};function showAccountTab(key){Object.entries(accountTabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)})}
function openAccountSettings(){el.set_provider.value=USER.provider||'openrouter';el.set_api_key_or.value=USER.apiKeyOpenRouter||'';el.set_api_key_oai.value=USER.apiKeyOpenAI||'';el.set_api_key_g.value=USER.apiKeyGoogle||'';el.set_api_key_claude.value=USER.apiKeyClaude||'';el.set_api_key_cf.value=USER.apiKeyCloudflare||'';el.set_api_key_custom1.value=USER.customKey1||'';el.set_api_key_custom1.value=USER.customKey1||'';el.set_master_prompt.value=USER.masterPrompt||'';el.set_title_model.value=USER.titleModel;el.set_gh_token.value=USER.githubToken||'';el.set_user_name.value=USER.name;el.userAvatarPreview.src=USER.avatar||'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';el.userAvatarPreview.classList.toggle('bg-gray-200',!USER.avatar);showAccountTab('General');el.accountSettingsModal.classList.remove('hidden')}
@@ -355,4 +195,4 @@ const getActiveJar=()=>!el.htmlEditor.classList.contains('hidden')?jars.html:jar
$(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,copyToClipboard,addSuneBubbleStreaming,clearChat,payloadWithSampling,setBtnStop,setBtnSend,localDemoReply,titleFrom,serializeThreadName,deserializeThreadName,ensureThreadOnFirstUser,generateTitleWithAI,threadRow,renderThreads,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,hideSuneSyncPopover,showSuneSyncPopover,hideSunesSyncPopover,showSunesSyncPopover,setSuneSyncStatus,checkSuneSyncStatus,performSuneDownload,performSuneUpload,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,suneStorage,gcStorage,cleanSuneStorage});
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,suneStorage,gcStorage,cleanSuneStorage});

View File

@@ -2,7 +2,6 @@ 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);
md.linkify.set({ fuzzyLink: true });
export function enhanceCodeBlocks(root, doHL = true) {
window.$(root).find('pre>code').each((i, code) => {

View File

@@ -4,8 +4,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.9.0/github-markdown-light.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.12.0/build/styles/github.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown-light.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css"/>
<link rel="stylesheet" href="/src/style.css"/>
<script defer src="https://cdn.jsdelivr.net/npm/cash-dom/dist/cash.min.js"></script>
<script defer src="//unpkg.com/alpinejs"></script>

View File

@@ -13,10 +13,6 @@
<button data-action="pfp" class="menu-item"><i data-lucide="image" class="h-4 w-4"></i><span>Change pfp</span></button>
<button data-action="export" class="menu-item"><i data-lucide="download" class="h-4 w-4"></i><span>Export sune (.sune)</span></button>
</div>
<div id="sunesSyncPopover" class="menu-card hidden">
<button id="sunesSyncUploadBtn" class="menu-item"><i data-lucide="upload-cloud" class="h-4 w-4"></i><span>Upload to GitHub</span></button>
<button id="sunesSyncDownloadBtn" class="menu-item"><i data-lucide="download-cloud" class="h-4 w-4"></i><span>Download from GitHub</span></button>
</div>
<div id="suneModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div>
<div class="absolute inset-x-0 top-12 mx-auto w-full max-w-md px-4">
@@ -25,7 +21,7 @@
<form id="settingsForm" class="text-sm">
<div class="border-b flex text-xs font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">HTML</button></div>
<div id="panelModel" class="p-4 space-y-4">
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(02)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(01)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
@@ -34,7 +30,7 @@
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(02)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(01)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(01)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Max Tokens (output)</label><input id="set_max_tokens" type="number" min="1" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder=""/></div>
<div><label class="block text-gray-700 font-medium mb-1">Verbosity</label><select id="set_verbosity" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div>
</div>
<div class="flex flex-wrap items-center gap-2 pt-2">
<div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div>
@@ -70,7 +66,6 @@
</div>
<div id="panelPrompt" class="p-4 space-y-4 hidden">
<div><div class="flex items-center justify-between mb-1"><label for="set_system_prompt" class="block text-gray-700 font-medium">System Prompt</label><div class="flex gap-2"><button type="button" id="copySystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteSystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Paste</button></div></div><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Enter a system prompt to guide the sune"></textarea></div>
<p class="text-xs text-gray-500">Use <code>{{model_name}}</code> to insert the model name.</p>
</div>
<div id="panelScript" class="p-1 hidden">
<div class="border-b flex text-xs font-medium"><button type="button" id="htmlTab_index" class="flex-1 py-2 px-3 text-center border-b-2"></button><button type="button" id="htmlTab_extension" class="flex-1 py-2 px-3 text-center border-b-2"></button></div>

View File

@@ -1,10 +1,10 @@
<div id="sidebarOverlayLeft" class="fixed inset-0 z-40 bg-black/20 hidden" @click="document.getElementById('sidebarLeft').classList.add('-translate-x-full');$el.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full');document.getElementById('sidebarOverlayRight').classList.add('hidden');hideThreadPopover();hideSunePopover();hideSunesSyncPopover()"></div>
<div id="sidebarOverlayLeft" class="fixed inset-0 z-40 bg-black/20 hidden" @click="document.getElementById('sidebarLeft').classList.add('-translate-x-full');$el.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full');document.getElementById('sidebarOverlayRight').classList.add('hidden');hideThreadPopover();hideSunePopover()"></div>
<aside id="sidebarLeft" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
<div class="p-2 border-b flex flex-col gap-2">
<input id="suneRepoInput" type="text" placeholder="gh://owner/sunes" class="w-full h-9 rounded-lg border-0 bg-gray-100 px-3 text-xs font-mono focus:ring-2 focus:ring-black focus:bg-white"/>
<div class="flex items-center justify-between gap-2">
<button id="newSuneBtn" class="px-3 py-1.5 rounded-lg bg-black text-white text-[10px] font-bold uppercase tracking-wider hover:bg-black/90 transition">New Sune</button>
<button id="suneSyncBtn" class="relative px-3 py-1.5 rounded-lg bg-gray-100 text-gray-900 text-[10px] font-bold uppercase tracking-wider hover:bg-gray-200 transition flex items-center gap-1"><i data-lucide="refresh-cw" class="h-3 w-3"></i><span>Sync</span><span id="suneSyncBadge" class="absolute -top-1 -right-1 block h-2.5 w-2.5 rounded-full ring-2 ring-white hidden"></span></button>
<button id="suneSyncBtn" class="px-3 py-1.5 rounded-lg bg-gray-100 text-gray-900 text-[10px] font-bold uppercase tracking-wider hover:bg-gray-200 transition flex items-center gap-1"><i data-lucide="refresh-cw" class="h-3 w-3"></i><span>Sync</span></button>
</div>
</div>
<div id="suneList" class="flex-1 overflow-y-auto divide-y"></div>

View File

@@ -1,7 +1,7 @@
<header id="topbar" 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-2 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">
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
<button id="suneBtnTop" class="justify-self-center h-10 w-10 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings"></button>
<button id="suneBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings"></button>
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
</div>
</header>

View File

@@ -9,8 +9,7 @@ export const buildBody=()=>{
msgs.push({role:'system', content: mPrompt});
}
const modelName=SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/,'').replace(/^[^/]*\//,'').replace(/(?::online|:free)+$/,'');
const sPrompt = (SUNE.system_prompt || '').trim().replaceAll('{{model_name}}',()=>modelName);
const sPrompt = (SUNE.system_prompt || '').trim();
if(sPrompt) {
msgs.push({role:'system', content: sPrompt});
}
@@ -39,6 +38,7 @@ export const buildBody=()=>{
const b=payloadWithSampling({model:SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/,''),messages:msgs,stream:true});
b.reasoning={...(SUNE.reasoning_effort&&SUNE.reasoning_effort!=='default'?{effort:SUNE.reasoning_effort}:{}),exclude:!SUNE.include_thoughts};
if(SUNE.verbosity)b.verbosity=SUNE.verbosity;
if(SUNE.img_output){b.modalities=['image'];b.image_config={aspect_ratio:SUNE.aspect_ratio||'1:1',image_size:SUNE.image_size||'1K'}}
return b
}

View File

@@ -16,7 +16,7 @@ export const fmtSize = b => {
return (x >= 10 ? Math.round(x) : Math.round(x * 10) / 10) + ' ' + u[i];
};
export const asDataURL = f => new Promise(r => { const fr = new FileReader(); fr.onload = () => r(String(fr.result || '')); fr.readAsDataURL(f); });
export const imgToWebp = (f, D = 96, q = 75) => new Promise((r, j) => {
export const imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => {
if (!f) return j();
const i = new Image;
i.onload = () => {