Files
sune/dist/assets/index-Bc9CPRwv.js
2026-03-13 02:07:03 +00:00

2392 lines
75 KiB
JavaScript

import mathjax3 from "https://esm.sh/markdown-it-mathjax3";
//#region \0vite/modulepreload-polyfill.js
(function polyfill() {
const relList = document.createElement("link").relList;
if (relList && relList.supports && relList.supports("modulepreload")) return;
for (const link of document.querySelectorAll("link[rel=\"modulepreload\"]")) processPreload(link);
new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type !== "childList") continue;
for (const node of mutation.addedNodes) if (node.tagName === "LINK" && node.rel === "modulepreload") processPreload(node);
}
}).observe(document, {
childList: true,
subtree: true
});
function getFetchOpts(link) {
const fetchOpts = {};
if (link.integrity) fetchOpts.integrity = link.integrity;
if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy;
if (link.crossOrigin === "use-credentials") fetchOpts.credentials = "include";
else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit";
else fetchOpts.credentials = "same-origin";
return fetchOpts;
}
function processPreload(link) {
if (link.ep) return;
link.ep = true;
const fetchOpts = getFetchOpts(link);
fetch(link.href, fetchOpts);
}
})();
//#endregion
//#region src/streaming.js
var HTTP_BASE = "https://us.proxy.sune.chat/ws";
var buildBody = () => {
const { USER, SUNE, state, payloadWithSampling } = window;
const msgs = [];
const mPrompt = (USER.masterPrompt || "").trim();
if (mPrompt && !SUNE.ignore_master_prompt) msgs.push({
role: "system",
content: mPrompt
});
const sPrompt = (SUNE.system_prompt || "").trim();
if (sPrompt) msgs.push({
role: "system",
content: sPrompt
});
state.messages.filter((m) => m.role !== "system").forEach((m) => {
let content = Array.isArray(m.content) ? [...m.content] : [{
type: "text",
text: String(m.content || "")
}];
content = content.filter((p) => p.type !== "text" || p.text && p.text.trim().length > 0);
msgs.push({
role: m.role,
content,
...m.images?.length ? { images: m.images } : {}
});
});
if (msgs.length > 0) {
const last = msgs[msgs.length - 1];
if (last.role === "assistant" && last.content.length === 0 && (!last.images || last.images.length === 0)) msgs.pop();
}
const b = payloadWithSampling({
model: SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/, ""),
messages: msgs,
stream: true
});
if (SUNE.json_output) {
let s;
try {
s = JSON.parse(SUNE.json_schema || "null");
} catch {
s = null;
}
if (s && typeof s === "object" && Object.keys(s).length > 0) b.response_format = {
type: "json_schema",
json_schema: s
};
else b.response_format = { type: "json_object" };
}
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;
};
async function streamORP(body, onDelta, streamId) {
const { USER, SUNE, state, gid, cacheStore } = window;
const model = SUNE.model, provider = model.startsWith("oai:") ? "openai" : model.startsWith("g:") ? "google" : model.startsWith("cla:") ? "claude" : model.startsWith("cf:") ? "cloudflare" : model.startsWith("or:") ? "openrouter" : USER.provider;
const apiKey = provider === "openai" ? USER.apiKeyOpenAI : provider === "google" ? USER.apiKeyGoogle : provider === "claude" ? USER.apiKeyClaude : provider === "cloudflare" ? USER.apiKeyCloudflare : USER.apiKeyOpenRouter;
if (!apiKey) {
onDelta(window.localDemoReply(), true);
return {
ok: true,
rid: streamId || null
};
}
const r = {
rid: streamId || gid(),
seq: -1,
done: false,
signaled: false,
ws: null
};
await cacheStore.setItem(r.rid, "busy");
const signal = (t) => {
if (!r.signaled) {
r.signaled = true;
onDelta(t || "", true);
}
};
const ws = new WebSocket(HTTP_BASE.replace("https", "wss") + "?uid=" + encodeURIComponent(r.rid));
r.ws = ws;
ws.onopen = () => ws.send(JSON.stringify({
type: "begin",
rid: r.rid,
provider,
apiKey,
or_body: body
}));
ws.onmessage = (e) => {
let m;
try {
m = JSON.parse(e.data);
} catch {
return;
}
if (m.type === "delta" && typeof m.seq === "number" && m.seq > r.seq) {
r.seq = m.seq;
onDelta(m.text || "", false, m.images);
} else if (m.type === "done" || m.type === "err") {
r.done = true;
cacheStore.setItem(r.rid, "done");
signal(m.type === "err" ? "\n\n" + (m.message || "error") : "");
ws.close();
}
};
ws.onclose = () => {};
ws.onerror = () => {};
state.controller = {
abort: () => {
r.done = true;
cacheStore.setItem(r.rid, "done");
try {
if (ws.readyState === 1) ws.send(JSON.stringify({
type: "stop",
rid: r.rid
}));
} catch {}
signal("");
},
disconnect: () => ws.close()
};
return {
ok: true,
rid: r.rid
};
}
async function streamChat(onDelta, streamId) {
return await streamORP(buildBody(), onDelta, streamId);
}
//#endregion
//#region src/sune-logo.js
var SUNE_LOGO_SVG = `
<div class="flex items-center justify-start py-1 opacity-80">
<style>
.s-spikes-pulse { transform-origin: 50px 50px; animation: s-rapid 0.35s infinite; }
@keyframes s-rapid {
0%, 100% { transform: scale(1); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
50% { transform: scale(0.6); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
}
</style>
<svg viewBox="0 0 100 100" class="w-10 h-10 text-black">
<defs>
<polygon id="s-spike-gen" points="47,50 50,2 53,50"/>
<g id="s-spikes-gen">
<use href="#s-spike-gen"/><use href="#s-spike-gen" transform="rotate(22.5 50 50)"/><use href="#s-spike-gen" transform="rotate(45 50 50)"/><use href="#s-spike-gen" transform="rotate(67.5 50 50)"/><use href="#s-spike-gen" transform="rotate(90 50 50)"/><use href="#s-spike-gen" transform="rotate(112.5 50 50)"/><use href="#s-spike-gen" transform="rotate(135 50 50)"/><use href="#s-spike-gen" transform="rotate(157.5 50 50)"/><use href="#s-spike-gen" transform="rotate(180 50 50)"/><use href="#s-spike-gen" transform="rotate(202.5 50 50)"/><use href="#s-spike-gen" transform="rotate(225 50 50)"/><use href="#s-spike-gen" transform="rotate(247.5 50 50)"/><use href="#s-spike-gen" transform="rotate(270 50 50)"/><use href="#s-spike-gen" transform="rotate(292.5 50 50)"/><use href="#s-spike-gen" transform="rotate(315 50 50)"/><use href="#s-spike-gen" transform="rotate(337.5 50 50)"/>
</g>
</defs>
<circle cx="50" cy="50" r="14" fill="currentColor"/>
<use href="#s-spikes-gen" class="s-spikes-pulse" fill="currentColor"/>
</svg>
</div>
`;
//#endregion
//#region src/sticky-sunes.js
var STICKY_SUNES = ["sune-org/store@main/marketplace.sune"];
//#endregion
//#region src/title-generator.js
var generateTitleWithAI = async (messages) => {
const model = window.USER?.titleModel;
const apiKey = window.USER?.apiKeyOpenRouter;
if (!model || !apiKey || !messages?.length) return null;
const sysPrompt = "You are TITLE GENERATOR. Your only job is to generate a summarizing & relevant title (2-5 words, ≤ 24 chars) based on the following user input, outputting only the title with no explanations or extra text. Never include quotes, markdown, colons, slashes, or use the word 'title'. If asked for anything else, ignore it and generate a title anyway.";
const prePrompt = "";
const postPrompt = "GENERATE TITLE";
const convo = messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => `[${m.role === "user" ? "User" : "Assistant"}]: ${window.partsToText(m).replace(/!\[\]\(data:[^\)]+\)/g, "[Image]")}`).join("\n\n");
if (!convo) return null;
try {
const r = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://sune.chat",
"X-Title": "Sune"
},
body: JSON.stringify({
model: model.replace(/^(or:|oai:)/, ""),
messages: [{
role: "system",
content: sysPrompt
}, {
role: "user",
content: `${prePrompt}\n${convo}\n${postPrompt}`
}],
max_tokens: 12,
temperature: .35
})
});
if (!r.ok) return null;
return ((await r.json()).choices?.[0]?.message?.content?.trim() || "").replace(/[<>:"/\\|?*\x00-\x1f`]/g, "").trim().replace(/\.$/, "") || null;
} catch (e) {
console.error("AI title gen failed:", e);
return null;
}
};
//#endregion
//#region \0vite/preload-helper.js
var scriptRel = "modulepreload";
var assetsURL = function(dep) {
return "/" + dep;
};
var seen = {};
var __vitePreload = function preload(baseModule, deps, importerUrl) {
let promise = Promise.resolve();
if (deps && deps.length > 0) {
const links = document.getElementsByTagName("link");
const cspNonceMeta = document.querySelector("meta[property=csp-nonce]");
const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce");
function allSettled(promises) {
return Promise.all(promises.map((p) => Promise.resolve(p).then((value) => ({
status: "fulfilled",
value
}), (reason) => ({
status: "rejected",
reason
}))));
}
promise = allSettled(deps.map((dep) => {
dep = assetsURL(dep, importerUrl);
if (dep in seen) return;
seen[dep] = true;
const isCss = dep.endsWith(".css");
const cssSelector = isCss ? "[rel=\"stylesheet\"]" : "";
if (!!importerUrl) for (let i = links.length - 1; i >= 0; i--) {
const link = links[i];
if (link.href === dep && (!isCss || link.rel === "stylesheet")) return;
}
else if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) return;
const link = document.createElement("link");
link.rel = isCss ? "stylesheet" : scriptRel;
if (!isCss) link.as = "script";
link.crossOrigin = "";
link.href = dep;
if (cspNonce) link.setAttribute("nonce", cspNonce);
document.head.appendChild(link);
if (isCss) return new Promise((res, rej) => {
link.addEventListener("load", res);
link.addEventListener("error", () => rej(/* @__PURE__ */ new Error(`Unable to preload CSS for ${dep}`)));
});
}));
}
function handlePreloadError(err) {
const e = new Event("vite:preloadError", { cancelable: true });
e.payload = err;
window.dispatchEvent(e);
if (!e.defaultPrevented) throw err;
}
return promise.then((res) => {
for (const item of res || []) {
if (item.status !== "rejected") continue;
handlePreloadError(item.reason);
}
return baseModule().catch(handlePreloadError);
});
};
//#endregion
//#region src/main.js
(() => {
let k, v = visualViewport;
const f = () => {
removeEventListener("popstate", f), document.activeElement?.blur();
};
v.onresize = () => {
let o = v.height < innerHeight;
o != k && ((k = o) ? (history.pushState({ k: 1 }, ""), addEventListener("popstate", f)) : (removeEventListener("popstate", f), history.state?.k && history.back()));
};
})();
var DEFAULT_MODEL = "anthropic/claude-opus-4.6";
var el = window.el = Object.fromEntries([
"topbar",
"chat",
"messages",
"composer",
"input",
"sendBtn",
"suneBtnTop",
"suneModal",
"suneURL",
"settingsForm",
"closeSettings",
"cancelSettings",
"tabModel",
"tabPrompt",
"tabScript",
"panelModel",
"panelPrompt",
"panelScript",
"set_model",
"set_temperature",
"set_top_p",
"set_top_k",
"set_frequency_penalty",
"set_repetition_penalty",
"set_min_p",
"set_top_a",
"set_verbosity",
"set_reasoning_effort",
"set_system_prompt",
"set_hide_composer",
"set_include_thoughts",
"set_json_output",
"set_img_output",
"set_aspect_ratio",
"set_image_size",
"aspectRatioContainer",
"set_ignore_master_prompt",
"deleteSuneBtn",
"sidebarLeft",
"sidebarOverlayLeft",
"sidebarBtnLeft",
"suneList",
"newSuneBtn",
"userMenuBtn",
"userMenu",
"accountSettingsOption",
"sunesImportOption",
"sunesExportOption",
"threadsImportOption",
"importInput",
"sidebarBtnRight",
"sidebarRight",
"sidebarOverlayRight",
"threadList",
"closeThreads",
"threadPopover",
"sunePopover",
"footer",
"attachBtn",
"attachBadge",
"fileInput",
"htmlEditor",
"extensionHtmlEditor",
"jsonSchemaEditor",
"htmlTab_index",
"htmlTab_extension",
"suneHtml",
"accountSettingsModal",
"accountSettingsForm",
"closeAccountSettings",
"cancelAccountSettings",
"set_master_prompt",
"set_provider",
"set_api_key_or",
"set_api_key_oai",
"set_api_key_g",
"set_api_key_claude",
"set_api_key_cf",
"set_title_model",
"copySystemPrompt",
"pasteSystemPrompt",
"copyHTML",
"pasteHTML",
"accountTabGeneral",
"accountTabAPI",
"accountPanelGeneral",
"accountPanelAPI",
"set_gh_token",
"gcpSAInput",
"gcpSAUploadBtn",
"importAccountSettings",
"exportAccountSettings",
"importAccountSettingsInput",
"accountTabUser",
"accountPanelUser",
"set_user_name",
"userAvatarPreview",
"setUserAvatarBtn",
"userAvatarInput",
"threadRepoInput",
"threadBackBtn",
"threadFolderBtn",
"threadSyncBtn"
].map((id) => [id, $("#" + id)[0]]));
var icons = () => window.lucide && lucide.createIcons();
var haptic = () => /android/i.test(navigator.userAgent) && navigator.vibrate?.(1);
var clamp = (v, min, max) => Math.max(min, Math.min(max, v)), num = (v, d) => v == null || v === "" || isNaN(+v) ? d : +v, int = (v, d) => v == null || v === "" || isNaN(parseInt(v)) ? d : parseInt(v), gid = () => Math.random().toString(36).slice(2, 9), esc = (s) => String(s).replace(/[&<>'"`]/g, (c) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;",
"`": "&#96;"
})[c]), positionPopover = (a, p) => {
const r = a.getBoundingClientRect();
p.style.top = `${r.bottom + p.offsetHeight + 4 > window.innerHeight ? r.top - p.offsetHeight - 4 : r.bottom + 4}px`;
p.style.left = `${Math.max(8, Math.min(r.right - p.offsetWidth, window.innerWidth - p.offsetWidth - 8))}px`;
};
var sid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
var fmtSize = (b) => {
const u = [
"B",
"KB",
"MB",
"GB",
"TB"
];
let i = 0, x = b;
while (x >= 1024 && i < u.length - 1) {
x /= 1024;
i++;
}
return (x >= 10 ? Math.round(x) : Math.round(x * 10) / 10) + " " + u[i];
};
var asDataURL = (f) => new Promise((r) => {
const fr = new FileReader();
fr.onload = () => r(String(fr.result || ""));
fr.readAsDataURL(f);
});
var imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => {
if (!f) return j();
const i = new Image();
i.onload = () => {
const c = document.createElement("canvas"), x = c.getContext("2d");
let w = i.width, h = i.height;
if (D > 0 && Math.max(w, h) > D) w > h ? (h = D * h / w, w = D) : (w = D * w / h, h = D);
c.width = w;
c.height = h;
x.drawImage(i, 0, 0, w, h);
r(c.toDataURL("image/webp", clamp(q, 0, 100) / 100));
URL.revokeObjectURL(i.src);
};
i.onerror = j;
i.src = URL.createObjectURL(f);
});
var b64 = (x) => x.split(",")[1] || "";
var utob = (s) => btoa(unescape(encodeURIComponent(s))), btou = (s) => decodeURIComponent(escape(atob(s.replace(/\s/g, ""))));
var ghApi = async (path, method = "GET", body = null) => {
const t = USER.githubToken;
if (!t) throw new Error("No GH token");
const r = await fetch(`https://api.github.com/repos/${path}`, {
method,
headers: {
"Authorization": `token ${t}`,
"Accept": "application/vnd.github.v3+json",
"Content-Type": "application/json"
},
body: body ? JSON.stringify(body) : null
});
if (!r.ok && r.status !== 404) throw new Error(`GH API ${r.status}`);
return r.status === 404 ? null : r.json();
};
var parseGhUrl = (u) => {
const p = u.substring(5).split("/"), owner = p[0], repoPart = p[1] || "", branch = repoPart.includes("@") ? repoPart.split("@")[1] : "main", repo = repoPart.split("@")[0], path = p.slice(2).join("/").replace(/\/$/, "");
return {
owner,
repo,
branch,
path,
apiPath: `${owner}/${repo}/contents${path ? "/" + path : ""}`
};
};
var 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 || "");
}
};
var 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,
json_output: false,
img_output: false,
aspect_ratio: "1:1",
image_size: "1K",
ignore_master_prompt: false,
json_schema: ""
};
var 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 || {}),
storage: p.storage || {}
});
var sunes = (su.load() || []).map(makeSune);
var SUNE = window.SUNE = new Proxy({
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);
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;
renderMarkdown(suneBubble, partsToText(assistantMsg), { enhance: false });
assistantMsg.content[0].text = buf;
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, 4e3));
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|storage)$/.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);
}
var state = window.state = {
messages: [],
busy: false,
controller: null,
currentThreadId: null,
abortRequested: false,
attachments: [],
stream: {
rid: null,
bubble: null,
meta: null,
text: "",
done: false
}
};
var getModelShort = (m) => {
const mm = m || SUNE.model || "";
return mm.includes("/") ? mm.split("/").pop() : mm;
};
var resolveSuneSrc = (src) => {
if (!src) return null;
if (src.startsWith("gh://")) {
const parts = src.substring(5).split("/");
if (parts.length < 3) return null;
const [owner, repo, ...filePathParts] = parts;
return `https://raw.githubusercontent.com/${owner}/${repo}/main/${filePathParts.join("/")}`;
}
return src;
};
var processSuneIncludes = async (html, depth = 0) => {
if (depth > 5) return "<!-- Sune include depth limit reached -->";
if (!html) return "";
const c = document.createElement("div");
c.innerHTML = html;
for (const n of [...c.querySelectorAll("sune")]) if (n.hasAttribute("src")) {
if (n.hasAttribute("private") && depth > 0) {
n.remove();
continue;
}
const s = n.getAttribute("src"), u = resolveSuneSrc(s);
if (!u) {
n.replaceWith(document.createComment(` Invalid src: ${esc(s)} `));
continue;
}
try {
const r = await fetch(u);
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const d = await r.json(), o = Array.isArray(d) ? d[0] : d, h = [o?.settings?.extension_html || "", o?.settings?.html || ""].join("\n");
n.replaceWith(document.createRange().createContextualFragment(await processSuneIncludes(h, depth + 1)));
} catch (e) {
n.replaceWith(document.createComment(` Fetch failed: ${esc(u)} `));
}
} else n.replaceWith(document.createRange().createContextualFragment(n.innerHTML));
return c.innerHTML;
};
var renderSuneHTML = async () => {
const h = await processSuneIncludes([SUNE.extension_html, SUNE.html].map((x) => (x || "").trim()).join("\n")), c = el.suneHtml;
c.innerHTML = "";
const t = h.trim();
c.classList.toggle("hidden", !t);
t && (c.appendChild(document.createRange().createContextualFragment(h)), window.Alpine?.initTree(c));
};
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-8 w-8 rounded-full object-cover"/>` : "✺";
el.footer.classList.toggle("hidden", !!a.settings.hide_composer);
await renderSuneHTML();
icons();
};
var 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>`;
var renderSidebar = window.renderSidebar = () => {
const list = [...SUNE.list].sort((a, b) => b.pinned - a.pinned);
el.suneList.innerHTML = list.map(suneRow).join("");
icons();
};
function enhanceCodeBlocks(root, doHL = true) {
$(root).find("pre>code").each((i, code) => {
if (code.textContent.length > 2e5) return;
const $pre = $(code).parent().addClass("relative rounded-xl border border-gray-200");
if (!$pre.find(".code-actions").length) {
const len = code.textContent.length, countText = len >= 1e3 ? (len / 1e3).toFixed(1) + "K" : len;
const $btn = $("<button class=\"bg-slate-900 text-white rounded-lg py-1 px-2 text-xs opacity-85\">Copy</button>").on("click", async (e) => {
e.stopPropagation();
try {
await navigator.clipboard.writeText(code.innerText);
$btn.text("Copied");
setTimeout(() => $btn.text("Copy"), 1200);
} catch {}
});
const $container = $("<div class=\"code-actions absolute top-2 right-2 flex items-center gap-2\"></div>");
$container.append($(`<span class="text-xs text-gray-500">${countText} chars</span>`), $btn);
$pre.append($container);
}
if (doHL && window.hljs && code.textContent.length < 1e5) hljs.highlightElement(code);
});
}
var md = window.markdownit({
html: false,
linkify: true,
typographer: true,
breaks: true
}).use(mathjax3);
var getSuneLabel = (m) => {
return `${m && m.sune_name || SUNE.name} · ${getModelShort(m && m.model)}`;
};
function _createMessageRow(m) {
const role = typeof m === "string" ? m : m && m.role || "assistant", meta = typeof m === "string" ? {} : m || {}, isUser = role === "user", $row = $("<div class=\"flex flex-col gap-2\"></div>"), $head = $("<div class=\"flex items-center gap-2 px-4\"></div>"), $avatar = $("<div></div>");
const uAva = isUser ? USER.avatar : meta.avatar;
uAva ? $avatar.attr("class", "msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden").html(`<img src="${esc(uAva)}" class="h-full w-full object-cover">`) : $avatar.attr("class", `${isUser ? "bg-gray-900 text-white" : "bg-gray-200 text-gray-900"} msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center`).text(isUser ? "👤" : "✺");
const $name = $("<div class=\"text-xs font-medium text-gray-500\"></div>").text(isUser ? USER.name : getSuneLabel(meta));
const $deleteBtn = $("<button class=\"p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-red-500\" title=\"Delete message\"><i data-lucide=\"x\" class=\"h-4 w-4\"></i></button>").on("click", async (e) => {
e.stopPropagation();
state.messages = state.messages.filter((msg) => msg.id !== m.id);
$row.remove();
await THREAD.persist();
});
const $copyBtn = $("<button class=\"ml-auto p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-gray-600\" title=\"Copy message\"><i data-lucide=\"copy\" class=\"h-4 w-4\"></i></button>").on("click", async function(e) {
e.stopPropagation();
try {
await navigator.clipboard.writeText(partsToText(m));
$(this).html("<i data-lucide=\"check\" class=\"h-4 w-4 text-green-500\"></i>");
icons();
setTimeout(() => {
$(this).html("<i data-lucide=\"copy\" class=\"h-4 w-4\"></i>");
icons();
}, 1200);
} catch {}
});
$head.append($avatar, $name, $copyBtn, $deleteBtn);
const $bubble = $(`<div class="${(isUser ? "bg-gray-50 border border-gray-200" : "bg-gray-100") + " msg-bubble markdown-body rounded-none px-4 py-3 w-full"}"></div>`);
$row.append($head, $bubble);
return $row;
}
function msgRow(m) {
const $row = _createMessageRow(m);
$(el.messages).append($row);
queueMicrotask(() => {
el.chat.scrollTo({
top: el.chat.scrollHeight,
behavior: "smooth"
});
icons();
});
return $row.find(".msg-bubble")[0];
}
var renderMarkdown = window.renderMarkdown = function(node, text, opt = {
enhance: true,
highlight: true
}) {
node.innerHTML = md.render(text);
if (opt.enhance) enhanceCodeBlocks(node, opt.highlight);
};
function partsToText(m) {
if (!m) return "";
const c = m.content, i = m.images;
let t = Array.isArray(c) ? c.map((p) => p?.type === "text" ? p.text : p?.type === "image_url" ? `![](${p.image_url?.url || ""})` : p?.type === "file" ? `[${p.file?.filename || "file"}]` : p?.type === "input_audio" ? `(audio:${p.input_audio?.format || ""})` : "").join("\n") : String(c || "");
if (Array.isArray(i)) t += i.map((x) => `\n![](${x.image_url?.url})\n`).join("");
return t;
}
var 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;
};
var addSuneBubbleStreaming = (meta, id) => msgRow(Object.assign({
role: "assistant",
id
}, meta));
var clearChat = () => {
el.suneHtml.dispatchEvent(new CustomEvent("sune:unmount"));
state.messages = [];
el.messages.innerHTML = "";
state.attachments = [];
updateAttachBadge();
el.fileInput.value = "";
};
var 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 → Account & Backup to set your API key.";
}
var titleFrom = (t) => {
if (!t) return "Untitled";
return (typeof t === "string" ? t : Array.isArray(t) ? partsToText({ content: t }) : "Untitled").replace(/\s+/g, " ").trim().slice(0, 60) || "Untitled";
};
var serializeThreadName = (t) => {
const s = (t.title || "Untitled").replace(/[^a-zA-Z0-9]/g, "_").slice(0, 150);
return `${t.pinned ? "1" : "0"}-${t.updatedAt || Date.now()}-${t.id}-${s}.json`;
};
var deserializeThreadName = (n) => {
const p = n.replace(".json", "").split("-");
if (p.length < 4) return null;
return {
pinned: p[0] === "1",
updatedAt: parseInt(p[1]),
id: p[2],
title: p.slice(3).join("-").replace(/_/g, " "),
status: "synced",
type: "thread"
};
};
var TKEY = "threads_v1", THREAD = window.THREAD = {
list: [],
load: async function() {
const u = el.threadRepoInput.value.trim();
if (u.startsWith("gh://")) this.list = await localforage.getItem("rem_index_" + u.substring(5)).then((v) => Array.isArray(v) ? v : []) || [];
else this.list = await localforage.getItem(TKEY).then((v) => Array.isArray(v) ? v : []) || [];
},
save: async function() {
const u = el.threadRepoInput.value.trim();
if (u.startsWith("gh://")) await localforage.setItem("rem_index_" + u.substring(5), this.list.map((t) => {
const n = { ...t };
delete n.messages;
return n;
}));
else await localforage.setItem(TKEY, this.list.map((t) => {
const n = { ...t };
delete n.messages;
return n;
}));
},
get: function(id) {
return this.list.find((t) => t.id === id);
},
get active() {
return this.get(state.currentThreadId);
},
persist: async function(full = true) {
const id = state.currentThreadId;
if (!id) return;
const meta = this.get(id);
if (!meta) return;
const u = el.threadRepoInput.value.trim(), prefix = u.startsWith("gh://") ? "rem_t_" : "t_";
await localforage.setItem(prefix + id, [...state.messages]);
if (full) {
meta.updatedAt = Date.now();
if (u.startsWith("gh://") && meta.status !== "new") meta.status = "modified";
await this.save();
await renderThreads();
}
},
setTitle: async function(id, title) {
const th = this.get(id);
if (!th || !title) return;
th.title = titleFrom(title);
th.updatedAt = Date.now();
if (el.threadRepoInput.value.trim().startsWith("gh://") && th.status !== "new") th.status = "modified";
await this.save();
await renderThreads();
},
getLastAssistantMessageId: () => {
const a = [...el.messages.querySelectorAll(".msg-bubble")];
for (let i = a.length - 1; i >= 0; i--) {
const b = a[i], h = b.previousElementSibling;
if (!h) continue;
if (!/^\s*You\b/.test(h.textContent || "")) return b.dataset.mid || null;
}
return null;
}
};
var cacheStore = localforage.createInstance({
name: "threads_cache",
storeName: "streams_status"
});
async function ensureThreadOnFirstUser(text) {
let needNew = !state.currentThreadId;
if (state.messages.length === 0) state.currentThreadId = null;
if (state.currentThreadId && !THREAD.get(state.currentThreadId)) needNew = true;
if (!needNew) return;
const id = gid(), now = Date.now(), u = el.threadRepoInput.value.trim(), th = {
id,
title: "",
pinned: false,
updatedAt: now,
type: "thread"
};
if (u.startsWith("gh://")) th.status = "new";
state.currentThreadId = id;
THREAD.list.unshift(th);
await THREAD.save();
const prefix = u.startsWith("gh://") ? "rem_t_" : "t_";
await localforage.setItem(prefix + id, []);
await renderThreads();
}
var threadRow = (t) => {
const icon = t.type === "folder" ? "folder" : t.type === "file" ? "file-text" : "";
return `<div class=\"relative flex items-center gap-2 px-3 py-2 ${t.pinned ? "bg-yellow-50" : ""}\"><button data-open-thread=\"${t.id}\" data-type=\"${t.type || "thread"}\" class=\"flex-1 text-left truncate flex items-center gap-2\">${icon ? `<i data-lucide="${icon}" class="h-4 w-4"></i>` : ""}${t.pinned ? "📌 " : ""}${esc(t.title || "Untitled")}${t.status === "modified" ? "*" : t.status === "new" ? "+" : ""}</button><button data-thread-menu=\"${t.id}\" class=\"h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center\" title=\"More\"><i data-lucide=\"more-horizontal\" class="h-4 w-4"></i></button></div>`;
};
var sortedThreads = [], isAddingThreads = false;
var THREAD_PAGE_SIZE = 50;
async function renderThreads() {
sortedThreads = [...THREAD.list].filter((t) => t.status !== "deleted").sort((a, b) => {
if (a.type === "file" && b.type !== "file") return -1;
if (a.type !== "file" && b.type === "file") return 1;
return b.pinned - a.pinned || b.updatedAt - a.updatedAt;
});
el.threadList.innerHTML = sortedThreads.slice(0, THREAD_PAGE_SIZE).map(threadRow).join("");
el.threadList.scrollTop = 0;
isAddingThreads = false;
icons();
}
var menuThreadId = null;
var hideThreadPopover = () => {
el.threadPopover.classList.add("hidden");
menuThreadId = null;
};
function showThreadPopover(btn, id) {
menuThreadId = id;
el.threadPopover.classList.remove("hidden");
positionPopover(btn, el.threadPopover);
icons();
}
var menuSuneId = null;
var hideSunePopover = () => {
el.sunePopover.classList.add("hidden");
menuSuneId = null;
};
function showSunePopover(btn, id) {
menuSuneId = id;
el.sunePopover.classList.remove("hidden");
positionPopover(btn, el.sunePopover);
icons();
}
$(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 && u.startsWith("gh://")) try {
const info = parseGhUrl(u), fileName = serializeThreadName(th), res = await ghApi(`${info.apiPath}/${fileName}?ref=${info.branch}`);
if (res && res.content) {
msgs = JSON.parse(btou(res.content));
await localforage.setItem(prefix + id, msgs);
th.status = "synced";
await THREAD.save();
}
} catch (e) {
console.error("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;
const c = el.threadList.children.length;
if (c >= sortedThreads.length) return;
isAddingThreads = true;
const b = sortedThreads.slice(c, c + THREAD_PAGE_SIZE);
if (b.length) {
el.threadList.insertAdjacentHTML("beforeend", b.map(threadRow).join(""));
icons();
}
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 >= 1e3 ? Math.round(tokens / 1e3) + "k" : String(tokens);
alert(tokens + " tokens (" + k + ")");
} else if (act === "export") {
const msgs = await localforage.getItem(prefix + th.id) || [];
dl(`thread-${(th.title || "thread").replace(/\W/g, "_")}-${ts()}.json`, {
...th,
messages: msgs
});
} else if (act === "copy_path") {
const u = el.threadRepoInput.value.trim();
if (u.startsWith("gh://")) {
const info = parseGhUrl(u);
try {
await navigator.clipboard.writeText(`${info.owner}/${info.repo}@${info.branch}/${th.id}`);
alert("Path copied.");
} catch {}
}
}
hideThreadPopover();
await THREAD.save();
renderThreads();
});
$(el.suneList).on("click", async (e) => {
const menuBtn = e.target.closest("[data-sune-menu]");
if (menuBtn) {
e.stopPropagation();
showSunePopover(menuBtn, menuBtn.getAttribute("[data-sune-menu]") ? menuBtn.getAttribute("[data-sune-menu]") : menuBtn.getAttribute("data-sune-menu"));
return;
}
const btn = e.target.closest("[data-sune-id]");
if (!btn) return;
const id = btn.getAttribute("data-sune-id");
if (id) {
if (state.busy) {
state.controller?.disconnect?.();
setBtnSend();
state.busy = false;
state.controller = null;
}
SUNE.setActive(id);
renderSidebar();
await reflectActiveSune();
state.currentThreadId = null;
clearChat();
document.getElementById("sidebarLeft").classList.add("-translate-x-full");
document.getElementById("sidebarOverlayLeft").classList.add("hidden");
}
});
$(el.sunePopover).on("click", async (e) => {
const act = e.target.closest("[data-action]")?.getAttribute("data-action");
if (!act || !menuSuneId) return;
const s = SUNE.get(menuSuneId);
if (!s) return;
const updateAndRender = async () => {
s.updatedAt = Date.now();
SUNE.save();
renderSidebar();
await reflectActiveSune();
};
if (act === "pin") {
s.pinned = !s.pinned;
await updateAndRender();
} else if (act === "rename") {
const n = prompt("Rename sune to:", s.name);
if (n != null) {
s.name = n.trim();
await updateAndRender();
}
} else if (act === "pfp") {
const i = document.createElement("input");
i.type = "file";
i.accept = "image/*";
i.onchange = async () => {
const f = i.files?.[0];
if (!f) return;
try {
s.avatar = await imgToWebp(f);
await updateAndRender();
} catch {}
};
i.click();
} else if (act === "export") dl(`sune-${(s.name || "sune").replace(/\W/g, "_")}-${ts()}.sune`, [s]);
hideSunePopover();
});
function updateAttachBadge() {
const n = state.attachments.length;
el.attachBadge.textContent = String(n);
el.attachBadge.classList.toggle("hidden", n === 0);
}
async function toAttach(file) {
if (!file) return null;
if (file instanceof File) {
const name = file.name || "file", mime = (file.type || "application/octet-stream").toLowerCase();
if (/^image\//.test(mime) || /\.(png|jpe?g|webp|gif)$/i.test(name)) return {
type: "image_url",
image_url: { url: mime === "image/webp" || /\.webp$/i.test(name) ? await asDataURL(file) : await imgToWebp(file, 2048, 94) }
};
if (mime === "application/pdf" || /\.pdf$/i.test(name)) {
const bin = b64(await asDataURL(file));
return {
type: "file",
file: {
filename: name.endsWith(".pdf") ? name : name + ".pdf",
file_data: bin
}
};
}
if (/^audio\//.test(mime) || /\.(wav|mp3)$/i.test(name)) return {
type: "input_audio",
input_audio: {
data: b64(await asDataURL(file)),
format: /mp3/.test(mime) || /\.mp3$/i.test(name) ? "mp3" : "wav"
}
};
return {
type: "file",
file: {
filename: name,
file_data: b64(await asDataURL(file))
}
};
}
if (file && file.name == null && file.data) {
const name = file.name || "file", mime = (file.mime || "application/octet-stream").toLowerCase();
if (/^image\//.test(mime)) return {
type: "image_url",
image_url: { url: `data:${mime};base64,${file.data}` }
};
if (mime === "application/pdf") return {
type: "file",
file: {
filename: name,
file_data: file.data
}
};
if (/^audio\//.test(mime)) {
const fmt = /mp3/.test(mime) ? "mp3" : "wav";
return {
type: "input_audio",
input_audio: {
data: file.data,
format: fmt
}
};
}
return {
type: "file",
file: {
filename: name,
file_data: file.data
}
};
}
return null;
}
$(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("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;
renderMarkdown(suneBubble, partsToText(assistantMsg), { enhance: false });
assistantMsg.content[0].text = buf;
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();
});
var jars = {
html: null,
extension: null,
jsonSchema: null
};
var ensureJars = async () => {
if (jars.html && jars.extension && jars.jsonSchema) return jars;
const mod = await __vitePreload(() => import("https://medv.io/codejar/codejar.js"), []), CodeJar = mod.CodeJar || mod.default, hl = (e) => e.innerHTML = hljs.highlight(e.textContent, { language: "xml" }).value, hl_json = (e) => e.innerHTML = hljs.highlight(e.textContent, { language: "json" }).value;
if (!jars.html) jars.html = CodeJar(el.htmlEditor, hl, { tab: " " });
if (!jars.extension) jars.extension = CodeJar(el.extensionHtmlEditor, hl, { tab: " " });
if (!jars.jsonSchema) jars.jsonSchema = CodeJar(el.jsonSchemaEditor, hl_json, { tab: " " });
return jars;
};
var 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_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_json_output.checked = !!s.json_output;
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");
}
var closeSettings = () => {
el.suneModal.classList.add("hidden");
};
var 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 === "Prompt") ensureJars().then(({ jsonSchema }) => {
const s = SUNE.settings;
jsonSchema.updateCode(s.json_schema || "");
});
else if (key === "Script") {
openedHTML = true;
showHtmlTab("index");
ensureJars().then(({ html, extension }) => {
const s = SUNE.settings;
html.updateCode(s.html || "");
extension.updateCode(s.extension_html || "");
});
}
}
$(el.suneBtnTop).on("click", openSettings);
$(el.cancelSettings).on("click", closeSettings);
$(el.suneModal).on("click", (e) => {
if (e.target === el.suneModal || e.target.classList.contains("bg-black/30")) closeSettings();
});
$(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"
].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.json_output = el.set_json_output.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;
SUNE.json_schema = el.jsonSchemaEditor.textContent;
if (openedHTML) {
SUNE.html = el.htmlEditor.textContent;
SUNE.extension_html = el.extensionHtmlEditor.textContent;
}
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");
});
function dl(name, obj) {
const blob = new Blob([JSON.stringify(obj, null, 2)], { type: name.endsWith(".sune") ? "application/octet-stream" : "application/json" }), url = URL.createObjectURL(blob), a = $("<a>").prop({
href: url,
download: name
}).appendTo("body");
a.get(0).click();
a.remove();
URL.revokeObjectURL(url);
}
var ts = () => {
const d = /* @__PURE__ */ new Date(), p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
};
var importMode = null;
$(el.sunesExportOption).on("click", () => {
dl(`sunes-${ts()}.sune`, {
version: 1,
sunes: SUNE.list,
activeId: SUNE.id
});
el.userMenu.classList.add("hidden");
});
$(el.sunesImportOption).on("click", () => {
importMode = "sunes";
el.importInput.value = "";
el.importInput.click();
});
$(el.threadsImportOption).on("click", () => {
importMode = "threads";
el.importInput.value = "";
el.importInput.click();
});
$(el.importInput).on("change", async () => {
const file = el.importInput.files?.[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
if (importMode === "sunes") {
const list = Array.isArray(data) ? data : Array.isArray(data.sunes) ? data.sunes : [];
if (!list.length) throw new Error("No sunes");
const incoming = list.map((a) => makeSune(a || {}));
const map = {};
incoming.forEach((s) => {
if (!s.id) s.id = gid();
const k = s.id, prev = map[k];
map[k] = !prev || +s.updatedAt > +prev.updatedAt ? s : prev;
});
let added = 0, updated = 0;
const idx = Object.fromEntries(sunes.map((s) => [s.id, s]));
Object.values(map).forEach((s) => {
const ex = idx[s.id];
if (!ex) {
sunes.push(s);
added++;
} else if (+s.updatedAt > +ex.updatedAt) {
Object.assign(ex, s);
updated++;
}
});
SUNE.save();
if (data.activeId && sunes.some((x) => x.id === data.activeId)) SUNE.setActive(data.activeId);
renderSidebar();
await reflectActiveSune();
state.currentThreadId = null;
clearChat();
alert(`${added} new, ${updated} updated.`);
} else if (importMode === "threads") {
if (!data || !data.id || !Array.isArray(data.messages)) throw new Error("Invalid thread format");
const u = el.threadRepoInput.value.trim(), prefix = u.startsWith("gh://") ? "rem_t_" : "t_";
const norm = (t) => ({
id: t.id || gid(),
title: titleFrom(t.title || t.messages),
pinned: !!t.pinned,
updatedAt: num(t.updatedAt, Date.now()),
type: "thread",
...u.startsWith("gh://") ? { status: "new" } : {}
});
const n = norm(data), msgs = data.messages, idx = THREAD.list.findIndex((x) => x.id === n.id);
if (idx > -1) {
if (n.updatedAt > THREAD.list[idx].updatedAt) {
THREAD.list[idx] = n;
await localforage.setItem(prefix + n.id, msgs);
}
} else {
THREAD.list.unshift(n);
await localforage.setItem(prefix + n.id, msgs);
}
await THREAD.save();
await renderThreads();
alert("Thread imported.");
}
el.userMenu.classList.add("hidden");
} catch {
alert("Import failed");
} finally {
importMode = null;
}
});
function kbUpdate() {
const vv = window.visualViewport;
const overlap = vv ? Math.max(0, window.innerHeight - (vv.height + vv.offsetTop)) : 0;
document.documentElement.style.setProperty("--kb", overlap + "px");
const fh = el.footer.getBoundingClientRect().height;
document.documentElement.style.setProperty("--footer-h", fh + "px");
el.footer.style.transform = "translateY(" + -overlap + "px)";
el.chat.style.scrollPaddingBottom = fh + overlap + 16 + "px";
}
function kbBind() {
if (window.visualViewport) ["resize", "scroll"].forEach((ev) => visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true }));
$(window).on("resize orientationchange", () => setTimeout(kbUpdate, 50));
$(el.input).on("focus click", () => {
setTimeout(() => {
kbUpdate();
el.input.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
}, 0);
});
}
function activeMeta() {
return {
sune_name: SUNE.name,
model: SUNE.model,
avatar: SUNE.avatar
};
}
var USER = window.USER = {
log: async (s) => {
const t = String(s ?? "").trim();
if (!t) return;
await ensureThreadOnFirstUser(t);
addMessage({
role: "user",
content: [{
type: "text",
text: t
}]
});
await THREAD.persist();
},
logMany: async (msgs) => {
if (!Array.isArray(msgs) || !msgs.length) return;
const clean = msgs.map((s) => String(s ?? "").trim()).filter(Boolean);
if (!clean.length) return;
await ensureThreadOnFirstUser(clean[0]);
const newMsgs = clean.map((t) => ({
id: gid(),
role: "user",
content: [{
type: "text",
text: t
}]
}));
state.messages.push(...newMsgs);
const frag = document.createDocumentFragment();
const newEls = newMsgs.map((m) => {
const $row = _createMessageRow(m), bubble = $row.find(".msg-bubble")[0];
bubble.dataset.mid = m.id;
return {
rowEl: $row[0],
bubbleEl: bubble,
message: m
};
});
newEls.forEach((item) => frag.appendChild(item.rowEl));
el.messages.appendChild(frag);
queueMicrotask(() => {
newEls.forEach((item) => {
renderMarkdown(item.bubbleEl, partsToText(item.message));
});
el.chat.scrollTo({
top: el.chat.scrollHeight,
behavior: "smooth"
});
icons();
});
await THREAD.persist();
},
get PAT() {
return this.githubToken;
},
get name() {
return localStorage.getItem("user_name") || "Anon";
},
set name(v) {
localStorage.setItem("user_name", v || "");
},
get avatar() {
return localStorage.getItem("user_avatar") || "";
},
set avatar(v) {
localStorage.setItem("user_avatar", v || "");
},
get provider() {
return localStorage.getItem("provider") || "openrouter";
},
set provider(v) {
localStorage.setItem("provider", [
"openai",
"google",
"claude"
].includes(v) ? v : "openrouter");
},
get apiKeyOpenRouter() {
return localStorage.getItem("openrouter_api_key") || "";
},
set apiKeyOpenRouter(v) {
localStorage.setItem("openrouter_api_key", v || "");
},
get apiKeyOpenAI() {
return localStorage.getItem("openai_api_key") || "";
},
set apiKeyOpenAI(v) {
localStorage.setItem("openai_api_key", v || "");
},
get apiKeyGoogle() {
return localStorage.getItem("google_api_key") || "";
},
set apiKeyGoogle(v) {
localStorage.setItem("google_api_key", v || "");
},
get apiKeyClaude() {
return localStorage.getItem("claude_api_key") || "";
},
set apiKeyClaude(v) {
localStorage.setItem("claude_api_key", v || "");
},
get apiKeyCloudflare() {
return localStorage.getItem("cloudflare_api_key") || "";
},
set apiKeyCloudflare(v) {
localStorage.setItem("cloudflare_api_key", v || "");
},
get apiKey() {
const p = this.provider;
return p === "openai" ? this.apiKeyOpenAI : p === "google" ? this.apiKeyGoogle : p === "claude" ? this.apiKeyClaude : p === "cloudflare" ? this.apiKeyCloudflare : this.apiKeyOpenRouter;
},
set apiKey(v) {
const p = this.provider;
if (p === "openai") this.apiKeyOpenAI = v;
else if (p === "google") this.apiKeyGoogle = v;
else if (p === "claude") this.apiKeyClaude = v;
else if (p === "cloudflare") this.apiKeyCloudflare = v;
else this.apiKeyOpenRouter = v;
},
get masterPrompt() {
return localStorage.getItem("master_prompt") || "Always respond using markdown.";
},
set masterPrompt(v) {
localStorage.setItem("master_prompt", v || "");
},
get titleModel() {
return localStorage.getItem("title_model") ?? "or:amazon/nova-micro-v1";
},
set titleModel(v) {
localStorage.setItem("title_model", v || "");
},
get githubToken() {
return localStorage.getItem("gh_token") || "";
},
set githubToken(v) {
localStorage.setItem("gh_token", v || "");
},
get gcpSA() {
try {
return JSON.parse(localStorage.getItem("gcp_sa_json") || "null");
} catch {
return null;
}
},
set gcpSA(v) {
localStorage.setItem("gcp_sa_json", v ? JSON.stringify(v) : "");
}
};
async function init() {
const u = localStorage.getItem("thread_repo_url") || "";
el.threadRepoInput.value = u;
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();
await reflectActiveSune();
clearChat();
icons();
kbBind();
kbUpdate();
}
$(window).on("resize", () => {
hideThreadPopover();
hideSunePopover();
});
var 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");
var pullThreads = async () => {
const u = el.threadRepoInput.value.trim();
if (!u.startsWith("gh://")) return;
const info = parseGhUrl(u);
try {
const items = await ghApi(`${info.apiPath}?ref=${info.branch}`);
if (!items) {
THREAD.list = [];
await THREAD.save();
} else {
THREAD.list = items.map((i) => {
if (i.type === "dir") return {
id: i.name,
title: i.name,
type: "folder",
updatedAt: 0
};
if (i.type === "file" && i.name.endsWith(".md")) return {
id: i.path,
title: i.name,
type: "file",
updatedAt: 0
};
const d = deserializeThreadName(i.name);
return d ? {
...d,
status: "synced"
} : null;
}).filter(Boolean);
await THREAD.save();
}
await renderThreads();
} catch (e) {
console.error("Auto-pull failed:", e);
}
};
$(el.threadRepoInput).on("change", async () => {
const u = el.threadRepoInput.value.trim();
localStorage.setItem("thread_repo_url", u);
if (state.currentThreadId) {
state.currentThreadId = null;
clearChat();
}
el.threadFolderBtn.classList.toggle("hidden", !u.startsWith("gh://"));
el.threadBackBtn.classList.toggle("hidden", !u.startsWith("gh://") || u.split("/").length <= 3);
if (u.startsWith("gh://")) await pullThreads();
else {
await THREAD.load();
await renderThreads();
}
});
$(el.threadBackBtn).on("click", () => {
const u = el.threadRepoInput.value.trim();
if (!u.startsWith("gh://")) return;
const p = u.split("/");
if (p.length > 3) {
p.pop();
el.threadRepoInput.value = p.join("/");
el.threadRepoInput.dispatchEvent(new Event("change"));
}
});
$(el.threadFolderBtn).on("click", async () => {
const n = prompt("Folder name:");
if (!n) return;
THREAD.list.unshift({
id: n.trim(),
title: n.trim(),
type: "folder",
updatedAt: Date.now()
});
await THREAD.save();
await renderThreads();
});
$(el.threadSyncBtn).on("click", async () => {
const u = el.threadRepoInput.value.trim();
if (!u.startsWith("gh://")) return;
const mode = confirm("Sync Threads:\nOK = Upload (Push)\nCancel = Download (Pull)");
const info = parseGhUrl(u);
try {
if (mode) {
const remoteItems = await ghApi(`${info.apiPath}?ref=${info.branch}`) || [], remoteMap = {};
remoteItems.forEach((i) => {
const d = deserializeThreadName(i.name);
if (d) remoteMap[d.id] = {
name: i.name,
sha: i.sha
};
});
const toRemove = [];
for (const t of THREAD.list) {
if (t.status === "deleted") {
if (remoteMap[t.id]) {
await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`, "DELETE", {
message: `Delete thread ${t.id}`,
sha: remoteMap[t.id].sha,
branch: info.branch
});
await localforage.removeItem("rem_t_" + t.id);
}
toRemove.push(t.id);
continue;
}
if (t.type !== "thread") continue;
if (t.status === "modified" || t.status === "new") {
const newName = serializeThreadName(t), msgs = await localforage.getItem("rem_t_" + t.id);
if (remoteMap[t.id] && remoteMap[t.id].name !== newName) await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`, "DELETE", {
message: `Rename thread ${t.id}`,
sha: remoteMap[t.id].sha,
branch: info.branch
});
const x = await ghApi(`${info.apiPath}/${newName}?ref=${info.branch}`);
await ghApi(`${info.apiPath}/${newName}`, "PUT", {
message: `Sync thread ${t.id}`,
content: utob(JSON.stringify(msgs, null, 2)),
branch: info.branch,
sha: x?.sha
});
t.status = "synced";
}
}
THREAD.list = THREAD.list.filter((x) => !toRemove.includes(x.id));
await THREAD.save();
alert("Pushed to GitHub.");
} else {
await pullThreads();
alert("Pulled from GitHub.");
}
await renderThreads();
} catch (e) {
alert("Sync failed: " + e.message);
}
});
init();
var accountTabs = {
General: ["accountTabGeneral", "accountPanelGeneral"],
API: ["accountTabAPI", "accountPanelAPI"],
User: ["accountTabUser", "accountPanelUser"]
};
function showAccountTab(key) {
Object.entries(accountTabs).forEach(([k, [tb, pn]]) => {
el[tb].classList.toggle("border-black", k === key);
el[pn].classList.toggle("hidden", k !== key);
});
}
function openAccountSettings() {
el.set_provider.value = USER.provider || "openrouter";
el.set_api_key_or.value = USER.apiKeyOpenRouter || "";
el.set_api_key_oai.value = USER.apiKeyOpenAI || "";
el.set_api_key_g.value = USER.apiKeyGoogle || "";
el.set_api_key_claude.value = USER.apiKeyClaude || "";
el.set_api_key_cf.value = USER.apiKeyCloudflare || "";
el.set_master_prompt.value = USER.masterPrompt || "";
el.set_title_model.value = USER.titleModel;
el.set_gh_token.value = USER.githubToken || "";
const sa = USER.gcpSA;
el.gcpSAUploadBtn.textContent = sa && sa.project_id ? `Uploaded: ${sa.project_id}` : "Upload .json";
el.set_user_name.value = USER.name;
el.userAvatarPreview.src = USER.avatar || "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
el.userAvatarPreview.classList.toggle("bg-gray-200", !USER.avatar);
showAccountTab("General");
el.accountSettingsModal.classList.remove("hidden");
}
function closeAccountSettings() {
el.accountSettingsModal.classList.add("hidden");
}
$(el.accountSettingsOption).on("click", () => {
el.userMenu.classList.add("hidden");
openAccountSettings();
});
$(el.closeAccountSettings).on("click", closeAccountSettings);
$(el.cancelAccountSettings).on("click", closeAccountSettings);
$(el.accountSettingsModal).on("click", (e) => {
if (e.target === el.accountSettingsModal || e.target.classList.contains("bg-black/30")) closeAccountSettings();
});
$(el.accountSettingsForm).on("submit", (e) => {
e.preventDefault();
USER.provider = el.set_provider.value || "openrouter";
USER.apiKeyOpenRouter = String(el.set_api_key_or.value || "").trim();
USER.apiKeyOpenAI = String(el.set_api_key_oai.value || "").trim();
USER.apiKeyGoogle = String(el.set_api_key_g.value || "").trim();
USER.apiKeyClaude = String(el.set_api_key_claude.value || "").trim();
USER.apiKeyCloudflare = String(el.set_api_key_cf.value || "").trim();
USER.masterPrompt = String(el.set_master_prompt.value || "").trim();
USER.titleModel = String(el.set_title_model.value || "").trim();
USER.githubToken = String(el.set_gh_token.value || "").trim();
USER.name = String(el.set_user_name.value || "").trim();
closeAccountSettings();
});
el.gcpSAUploadBtn.onclick = () => el.gcpSAInput.click();
el.gcpSAInput.onchange = async (e) => {
const f = e.target.files?.[0];
if (!f) return;
try {
const t = await f.text(), d = JSON.parse(t);
if (!d.project_id) throw new Error("Invalid");
USER.gcpSA = d;
el.gcpSAUploadBtn.textContent = `Uploaded: ${d.project_id}`;
alert("GCP SA loaded.");
} catch {
alert("Failed to load GCP SA.");
}
};
$(el.accountPanelAPI).on("click", (e) => {
const b = e.target.closest("[data-reveal-for]");
if (!b) return;
const i = document.getElementById(b.dataset.revealFor);
if (!i) return;
const p = i.type === "password";
i.type = p ? "text" : "password";
b.querySelector("i").setAttribute("data-lucide", p ? "eye-off" : "eye");
lucide.createIcons();
});
el.accountTabGeneral.onclick = () => showAccountTab("General");
el.accountTabAPI.onclick = () => showAccountTab("API");
el.accountTabUser.onclick = () => showAccountTab("User");
el.setUserAvatarBtn.onclick = () => el.userAvatarInput.click();
el.userAvatarInput.onchange = async (e) => {
const f = e.target.files?.[0];
if (!f) return;
try {
const dataUrl = await imgToWebp(f);
USER.avatar = dataUrl;
el.userAvatarPreview.src = dataUrl;
el.userAvatarPreview.classList.remove("bg-gray-200");
} catch {
alert("Failed to process image.");
}
};
el.exportAccountSettings.onclick = () => dl(`sune-account-${ts()}.json`, {
v: 1,
provider: USER.provider,
apiKeyOpenRouter: USER.apiKeyOpenRouter,
apiKeyOpenAI: USER.apiKeyOpenAI,
apiKeyGoogle: USER.apiKeyGoogle,
apiKeyClaude: USER.apiKeyClaude,
apiKeyCloudflare: USER.apiKeyCloudflare,
masterPrompt: USER.masterPrompt,
titleModel: USER.titleModel,
githubToken: USER.githubToken,
gcpSA: USER.gcpSA,
userName: USER.name,
userAvatar: USER.avatar
});
el.importAccountSettings.onclick = () => {
el.importAccountSettingsInput.value = "";
el.importAccountSettingsInput.click();
};
el.importAccountSettingsInput.onchange = async (e) => {
const f = e.target.files?.[0];
if (!f) return;
try {
const d = JSON.parse(await f.text());
if (!d || typeof d !== "object") throw new Error("Invalid");
Object.entries({
provider: "provider",
apiKeyOpenRouter: "apiKeyOR",
apiKeyOpenAI: "apiKeyOAI",
apiKeyGoogle: "apiKeyG",
apiKeyClaude: "apiKeyC",
apiKeyCloudflare: "apiKeyCF",
masterPrompt: "masterPrompt",
titleModel: "titleModel",
githubToken: "ghToken",
name: "userName",
avatar: "userAvatar",
gcpSA: "gcpSA"
}).forEach(([p, k]) => {
const v = d[p] ?? d[k];
if (typeof v === "string" || p === "gcpSA" && typeof v === "object" && v) USER[p] = v;
});
openAccountSettings();
alert("Imported.");
} catch {
alert("Import failed");
}
};
var getBubbleById = (id) => el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`);
async function syncActiveThread() {
const id = THREAD.getLastAssistantMessageId();
if (!id) return false;
if (await cacheStore.getItem(id) === "done") {
if (state.busy) {
setBtnSend();
state.busy = false;
state.controller = null;
}
return false;
}
if (!state.busy) {
state.busy = true;
state.controller = { abort: () => {
const ws = new WebSocket(HTTP_BASE.replace("https", "wss"));
ws.onopen = function() {
this.send(JSON.stringify({
type: "stop",
rid: id
}));
this.close();
};
} };
setBtnStop();
}
const bubble = getBubbleById(id);
if (!bubble) return false;
const msgIdx = state.messages.findIndex((x) => x.id === id);
const localText = msgIdx >= 0 ? partsToText(state.messages[msgIdx]) : bubble.textContent || "";
const j = await fetch(HTTP_BASE + "?uid=" + encodeURIComponent(id)).then((r) => r.ok ? r.json() : null).catch(() => null);
const finalise = (t, c, imgs) => {
renderMarkdown(bubble, partsToText({
content: c,
images: imgs
}), { enhance: false });
enhanceCodeBlocks(bubble, true);
if (msgIdx >= 0) {
state.messages[msgIdx].content = c;
state.messages[msgIdx].images = imgs;
} else state.messages.push({
id,
role: "assistant",
content: c,
images: imgs,
...activeMeta()
});
THREAD.persist();
setBtnSend();
state.busy = false;
cacheStore.setItem(id, "done");
state.controller = null;
el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: state.messages.find((m) => m.id === id) } }));
};
if (!j || j.rid !== id) {
if (j && j.error) {
const t = localText + "\n\n" + j.error;
finalise(t, [{
type: "text",
text: t
}]);
}
return false;
}
const serverText = j.text || "", isDone = j.error || j.done || j.phase === "done";
const finalText = serverText.length >= localText.length || isDone ? serverText : localText;
const display = partsToText({
content: [{
type: "text",
text: finalText
}],
images: j.images
});
if (display) renderMarkdown(bubble, display, { enhance: false });
if (isDone) {
if (finalText !== localText) finalise(finalText, [{
type: "text",
text: finalText
}], j.images);
else {
await cacheStore.setItem(id, "done");
if (state.busy) {
setBtnSend();
state.busy = false;
state.controller = null;
}
}
return false;
}
await cacheStore.setItem(id, "busy");
return true;
}
var syncLoopRunning = false;
async function syncWhileBusy() {
if (syncLoopRunning || document.visibilityState === "hidden") return;
syncLoopRunning = true;
try {
while (await syncActiveThread()) await new Promise((r) => setTimeout(r, 1500));
} finally {
syncLoopRunning = false;
}
}
var onForeground = () => {
if (document.visibilityState !== "visible") return;
state.controller?.disconnect?.();
if (state.busy) syncWhileBusy();
};
$(document).on("visibilitychange", onForeground);
$(el.copySystemPrompt).on("click", async () => {
try {
await navigator.clipboard.writeText(el.set_system_prompt.value || "");
} catch {}
});
$(el.pasteSystemPrompt).on("click", async () => {
try {
el.set_system_prompt.value = await navigator.clipboard.readText();
} catch {}
});
var getActiveHtmlParts = () => !el.htmlEditor.classList.contains("hidden") ? [el.htmlEditor, jars.html] : [el.extensionHtmlEditor, jars.extension];
$(el.copyHTML).on("click", async () => {
try {
await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent || "");
} catch {}
});
$(el.pasteHTML).on("click", async () => {
try {
const t = await navigator.clipboard.readText();
const [editor, jar] = getActiveHtmlParts();
if (jar && jar.updateCode) jar.updateCode(t);
else if (editor) editor.textContent = t;
} catch {}
});
Object.assign(window, {
icons,
haptic,
clamp,
num,
int,
gid,
esc,
positionPopover,
sid,
fmtSize,
asDataURL,
b64,
makeSune,
getModelShort,
resolveSuneSrc,
processSuneIncludes,
renderSuneHTML,
reflectActiveSune,
suneRow,
enhanceCodeBlocks,
getSuneLabel,
_createMessageRow,
msgRow,
partsToText,
addSuneBubbleStreaming,
clearChat,
payloadWithSampling,
setBtnStop,
setBtnSend,
localDemoReply,
titleFrom,
serializeThreadName,
deserializeThreadName,
ensureThreadOnFirstUser,
generateTitleWithAI,
threadRow,
renderThreads,
hideThreadPopover,
showThreadPopover,
hideSunePopover,
showSunePopover,
updateAttachBadge,
toAttach,
ensureJars,
openSettings,
closeSettings,
showTab,
dl,
ts,
kbUpdate,
kbBind,
activeMeta,
init,
showHtmlTab,
showAccountTab,
openAccountSettings,
closeAccountSettings,
getBubbleById,
syncActiveThread,
syncWhileBusy,
onForeground,
getActiveHtmlParts,
imgToWebp,
cacheStore,
ghApi,
parseGhUrl,
pullThreads
});
//#endregion