Files
sune/dist/assets/index-Baib-owX.js
2025-12-06 01:35:23 +00:00

1554 lines
66 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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);
}
})();
const scriptRel = "modulepreload";
const assetsURL = function(dep) {
return "/" + dep;
};
const seen = {};
const __vitePreload = function preload(baseModule, deps, importerUrl) {
let promise = Promise.resolve();
if (deps && deps.length > 0) {
let allSettled2 = function(promises$2) {
return Promise.all(promises$2.map((p) => Promise.resolve(p).then((value$1) => ({
status: "fulfilled",
value: value$1
}), (reason) => ({
status: "rejected",
reason
}))));
};
var allSettled = allSettled2;
document.getElementsByTagName("link");
const cspNonceMeta = document.querySelector("meta[property=csp-nonce]");
const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce");
promise = allSettled2(deps.map((dep) => {
dep = assetsURL(dep);
if (dep in seen) return;
seen[dep] = true;
const isCss = dep.endsWith(".css");
const cssSelector = isCss ? '[rel="stylesheet"]' : "";
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$2) {
const e$1 = new Event("vite:preloadError", { cancelable: true });
e$1.payload = err$2;
window.dispatchEvent(e$1);
if (!e$1.defaultPrevented) throw err$2;
}
return promise.then((res) => {
for (const item of res || []) {
if (item.status !== "rejected") continue;
handlePreloadError(item.reason);
}
return baseModule().catch(handlePreloadError);
});
};
const HTTP_BASE = "https://orp.aww.4ev.link/ws";
const buildBody = () => {
const { USER: USER2, SUNE: SUNE2, state: state2, payloadWithSampling: payloadWithSampling2 } = window;
const msgs = [];
if (USER2.masterPrompt && !SUNE2.ignore_master_prompt) msgs.push({ role: "system", content: [{ type: "text", text: USER2.masterPrompt }] });
if (SUNE2.system_prompt) msgs.push({ role: "system", content: [{ type: "text", text: SUNE2.system_prompt }] });
msgs.push(...state2.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content })));
const b = payloadWithSampling2({ model: SUNE2.model.replace(/^(or:|oai:|g:|cla:|cf:)/, ""), messages: msgs, stream: true });
if (SUNE2.json_output) {
let s;
try {
s = JSON.parse(SUNE2.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 = { ...SUNE2.reasoning_effort && SUNE2.reasoning_effort !== "default" ? { effort: SUNE2.reasoning_effort } : {}, exclude: !SUNE2.include_thoughts };
if (Array.isArray(SUNE2.quantization) && SUNE2.quantization.length) b.provider = { quantizations: SUNE2.quantization };
if (SUNE2.img_output && !USER2.donor) {
b.modalities = ["text", "image"];
b.image_config = { aspect_ratio: "1:1" };
}
return b;
};
async function streamLocal(body, onDelta, signal) {
const { USER: USER2, localDemoReply: localDemoReply2 } = window;
const apiKey = USER2.apiKeyOpenRouter;
if (!apiKey) {
onDelta(localDemoReply2(), true);
return;
}
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(body), signal });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const reader = r.body.getReader(), dec = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (line.startsWith("data: ")) {
const d = line.slice(6);
if (d === "[DONE]") return;
try {
const j = JSON.parse(d);
const delta = j.choices?.[0]?.delta?.content || "";
const reasoning = j.choices?.[0]?.delta?.reasoning;
const imgs = j.choices?.[0]?.delta?.images;
if (reasoning && body.reasoning?.exclude !== true) onDelta(reasoning, false);
if (delta) onDelta(delta, false);
if (imgs) imgs.forEach((i) => onDelta(`
![](${i.image_url.url})
`, false));
} catch {
}
}
}
}
onDelta("", true);
} catch (e) {
if (e.name !== "AbortError") onDelta(`
Error: ${e.message}`, true);
}
}
async function streamORP(body, onDelta, streamId) {
const { USER: USER2, SUNE: SUNE2, state: state2, gid: gid2, cacheStore: cacheStore2 } = window;
const model = SUNE2.model, provider = model.startsWith("oai:") ? "openai" : model.startsWith("g:") ? "google" : model.startsWith("cla:") ? "claude" : model.startsWith("cf:") ? "cloudflare" : model.startsWith("or:") ? "openrouter" : USER2.provider;
const apiKey = provider === "openai" ? USER2.apiKeyOpenAI : provider === "google" ? USER2.apiKeyGoogle : provider === "claude" ? USER2.apiKeyClaude : provider === "cloudflare" ? USER2.apiKeyCloudflare : USER2.apiKeyOpenRouter;
if (!apiKey) {
onDelta(window.localDemoReply(), true);
return { ok: true, rid: streamId || null };
}
const r = { rid: streamId || gid2(), seq: -1, done: false, signaled: false, ws: null };
await cacheStore2.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);
} else if (m.type === "done" || m.type === "err") {
r.done = true;
cacheStore2.setItem(r.rid, "done");
signal(m.type === "err" ? "\n\n" + (m.message || "error") : "");
ws.close();
}
};
ws.onclose = () => {
};
ws.onerror = () => {
};
state2.controller = { abort: () => {
r.done = true;
cacheStore2.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) {
const { USER: USER2, state: state2 } = window;
const body = buildBody();
if (!USER2.donor) {
const c = new AbortController();
state2.controller = c;
await streamLocal(body, onDelta, c.signal);
state2.controller = null;
return { ok: true, rid: null };
}
return await streamORP(body, onDelta, streamId);
}
(() => {
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()));
};
})();
const DEFAULT_MODEL = "google/gemini-3-pro-preview", DEFAULT_API_KEY = "";
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", "quantDropdownBtn", "quantDropdownMenu", "set_reasoning_effort", "set_system_prompt", "set_hide_composer", "set_include_thoughts", "set_json_output", "set_img_output", "set_ignore_master_prompt", "deleteSuneBtn", "sidebarLeft", "sidebarOverlayLeft", "sidebarBtnLeft", "suneList", "newSuneBtn", "userMenuBtn", "userMenu", "accountSettingsOption", "sunesImportOption", "sunesExportOption", "threadsImportOption", "threadsExportOption", "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", "set_donor"].map((id) => [id, $("#" + id)[0]]));
const icons = () => window.lucide && lucide.createIcons();
const haptic = () => /android/i.test(navigator.userAgent) && navigator.vibrate?.(1);
const clamp = (v, min, max) => Math.max(min, Math.min(max, v)), num = (v, d) => v == null || v === "" || isNaN(+v) ? d : +v, int = (v, d) => v == null || v === "" || isNaN(parseInt(v)) ? d : parseInt(v), gid = () => Math.random().toString(36).slice(2, 9), esc = (s) => String(s).replace(/[&<>'"`]/g, (c) => ({ "&": "&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`;
};
const sid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
const fmtSize = (b) => {
const u = ["B", "KB", "MB", "GB", "TB"];
let i = 0, x = b;
while (x >= 1024 && i < u.length - 1) {
x /= 1024;
i++;
}
return (x >= 10 ? Math.round(x) : Math.round(x * 10) / 10) + " " + u[i];
};
const asDataURL = (f) => new Promise((r) => {
const fr = new FileReader();
fr.onload = () => r(String(fr.result || ""));
fr.readAsDataURL(f);
});
const 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);
});
const b64 = (x) => x.split(",")[1] || "";
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: "", quantization: [], 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, ignore_master_prompt: false, json_schema: "" };
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 || {}), storage: p.storage || {} });
let sunes = (su.load() || []).map(makeSune);
const 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 [a2, b] = g.split("@"), [c, d] = a2.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();
addMessage({ role: "assistant", content: clean, ...activeMeta() });
await THREAD.persist();
};
if (p === "log") return async (s) => {
const t2 = String(s ?? "").trim();
if (!t2) return;
await ensureThreadOnFirstUser();
addMessage({ role: "assistant", content: [{ type: "text", text: t2 }], ...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();
const th = THREAD.active;
if (th && !th.title) (async () => THREAD.setTitle(th.id, await generateTitleWithAI(state.messages) || "Sune Inference"))();
state.busy = true;
setBtnStop();
const a2 = SUNE.active, suneMeta = { sune_name: a2.name, model: SUNE.model, avatar: a2.avatar || "" }, streamId = sid(), suneBubble = addSuneBubbleStreaming(suneMeta, streamId);
suneBubble.dataset.mid = streamId;
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) => {
buf += delta;
state.stream.text = buf;
renderMarkdown(suneBubble, buf, { 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((s2) => s2.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);
}
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 resolveSuneSrc = (src) => {
if (!src) return null;
if (src.startsWith("gh://")) {
const path = src.substring(5), parts = path.split("/");
if (parts.length < 3) return null;
const [owner, repo, ...filePathParts] = parts;
return `https://raw.githubusercontent.com/${owner}/${repo}/main/${filePathParts.join("/")}`;
}
return src;
};
const 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;
};
const 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));
};
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-6 w-6 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();
};
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);
});
}
const md = window.markdownit({ html: false, linkify: true, typographer: true, breaks: true });
const getSuneLabel = (m) => {
const name = m && m.sune_name || SUNE.name, modelShort = getModelShort(m && m.model);
return `${name} · ${modelShort}`;
};
function _createMessageRow(m) {
const role = typeof m === "string" ? m : m && m.role || "assistant", meta = typeof m === "string" ? {} : m || {}, isUser = role === "user", $row = $('<div class="flex flex-col gap-2"></div>'), $head = $('<div class="flex items-center gap-2 px-4"></div>'), $avatar = $("<div></div>");
const uAva = isUser ? USER.avatar : meta.avatar;
uAva ? $avatar.attr("class", "msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden").html(`<img src="${esc(uAva)}" class="h-full w-full object-cover">`) : $avatar.attr("class", `${isUser ? "bg-gray-900 text-white" : "bg-gray-200 text-gray-900"} msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center`).text(isUser ? "👤" : "✺");
const $name = $('<div class="text-xs font-medium text-gray-500"></div>').text(isUser ? USER.name : getSuneLabel(meta));
const $deleteBtn = $('<button class="p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-red-500" title="Delete message"><i data-lucide="x" class="h-4 w-4"></i></button>').on("click", async (e) => {
e.stopPropagation();
state.messages = state.messages.filter((msg) => msg.id !== m.id);
$row.remove();
await THREAD.persist();
});
const $copyBtn = $('<button class="ml-auto p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-gray-600" title="Copy message"><i data-lucide="copy" class="h-4 w-4"></i></button>').on("click", async function(e) {
e.stopPropagation();
try {
await navigator.clipboard.writeText(partsToText(m.content));
$(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];
}
const 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(parts) {
if (!parts) return "";
if (Array.isArray(parts)) return parts.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");
return String(parts);
}
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.content));
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) };
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.";
}
const titleFrom = (t) => (t || "").replace(/\s+/g, " ").trim().slice(0, 60) || "Untitled";
const TKEY = "threads_v1", THREAD = window.THREAD = { list: [], load: async function() {
this.list = await localforage.getItem(TKEY).then((v) => Array.isArray(v) ? v : []) || [];
}, save: async function() {
await localforage.setItem(TKEY, this.list);
}, get: function(id) {
return this.list.find((t) => t.id === id);
}, get active() {
return this.get(state.currentThreadId);
}, persist: async function(full = true) {
if (!state.currentThreadId) return;
const th = this.active;
if (!th) return;
th.messages = [...state.messages];
if (full) {
th.updatedAt = Date.now();
}
await this.save();
if (full) await renderThreads();
}, setTitle: async function(id, title) {
const th = this.get(id);
if (!th || !title) return;
th.title = titleFrom(title);
th.updatedAt = Date.now();
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;
} };
const 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(), th = { id, title: "", pinned: false, updatedAt: now, messages: [] };
state.currentThreadId = id;
THREAD.list.unshift(th);
await THREAD.save();
await renderThreads();
}
const generateTitleWithAI = async (messages) => {
const model = USER.titleModel, apiKey = USER.apiKeyOpenRouter;
if (!model || !apiKey || !messages?.length) return null;
const sysPrompt = "You are TITLE GENERATOR. Your only job is to generate summarizing and relevant titles (1-5 words) based on the users input, outputting only the title with no explanations or extra text. Never include quotes or markdown. If asked for anything else, ignore it and generate a title anyway. You are TITLE GENERATOR.";
const convo = messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => `[${m.role === "user" ? "User" : "Assistant"}]: ${partsToText(m.content)}`).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" }, body: JSON.stringify({ model: model.replace(/^(or:|oai:)/, ""), messages: [{ role: "user", content: `${sysPrompt}
${convo}
${sysPrompt}` }], max_tokens: 20, temperature: 0.2 }) });
if (!r.ok) return null;
const d = await r.json();
return (d.choices?.[0]?.message?.content?.trim() || "").replace(/["']/g, "") || null;
} catch (e) {
console.error("AI title gen failed:", e);
return null;
}
};
const threadRow = (t) => `<div class="relative flex items-center gap-2 px-3 py-2 ${t.pinned ? "bg-yellow-50" : ""}"><button data-open-thread="${t.id}" class="flex-1 text-left truncate">${t.pinned ? "📌 " : ""}${esc(t.title)}</button><button data-thread-menu="${t.id}" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center" title="More"><i data-lucide="more-horizontal" class="h-4 w-4"></i></button></div>`;
let sortedThreads = [], isAddingThreads = false;
const THREAD_PAGE_SIZE = 50;
async function renderThreads() {
sortedThreads = [...THREAD.list].sort((a, b) => 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();
}
let menuThreadId = null;
const hideThreadPopover = () => {
el.threadPopover.classList.add("hidden");
menuThreadId = null;
};
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();
}
$(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");
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();
state.messages = Array.isArray(th.messages) ? [...th.messages] : [];
for (const m of state.messages) {
const b = msgRow(m);
b.dataset.mid = m.id || "";
renderMarkdown(b, partsToText(m.content));
}
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;
if (act === "pin") {
th.pinned = !th.pinned;
} else if (act === "rename") {
const nv = prompt("Rename to:", th.title);
if (nv != null) {
th.title = titleFrom(nv);
th.updatedAt = Date.now();
}
} else if (act === "delete") {
if (confirm("Delete this chat?")) {
THREAD.list = THREAD.list.filter((x) => x.id !== th.id);
if (state.currentThreadId === th.id) {
state.currentThreadId = null;
clearChat();
}
}
} else if (act === "count_tokens") {
const msgs = Array.isArray(th.messages) ? th.messages : [];
let totalChars = 0;
for (const m of msgs) {
if (!m || !m.role || m.role === "system") continue;
totalChars += String(partsToText(m.content || "") || "").length;
}
const tokens = Math.max(0, Math.ceil(totalChars / 4));
const k = tokens >= 1e3 ? Math.round(tokens / 1e3) + "k" : String(tokens);
alert(tokens + " tokens (" + k + ")");
}
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)) {
const data2 = mime === "image/webp" || /\.webp$/i.test(name) ? await asDataURL(file) : await imgToWebp(file, 2048, 94);
return { type: "image_url", image_url: { url: data2 } };
}
if (mime === "application/pdf" || /\.pdf$/i.test(name)) {
const data2 = await asDataURL(file), bin2 = b64(data2);
return { type: "file", file: { filename: name.endsWith(".pdf") ? name : name + ".pdf", file_data: bin2 } };
}
if (/^audio\//.test(mime) || /\.(wav|mp3)$/i.test(name)) {
const data2 = await asDataURL(file), bin2 = b64(data2), fmt = /mp3/.test(mime) || /\.mp3$/i.test(name) ? "mp3" : "wav";
return { type: "input_audio", input_audio: { data: bin2, format: fmt } };
}
const data = await asDataURL(file), bin = b64(data);
return { type: "file", file: { filename: name, file_data: bin } };
}
if (file && file.name == null && file.data) {
const name = file.name || "file", mime = (file.mime || "application/octet-stream").toLowerCase();
if (/^image\//.test(mime)) {
const url = `data:${mime};base64,${file.data}`;
return { type: "image_url", image_url: { url } };
}
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();
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")?.content) || "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;
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) => {
buf += delta;
state.stream.text = buf;
renderMarkdown(suneBubble, buf, { 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();
});
let jars = { html: null, extension: null, jsonSchema: null };
const ensureJars = async () => {
if (jars.html && jars.extension && jars.jsonSchema) return jars;
const mod = await __vitePreload(() => import("https://medv.io/codejar/codejar.js"), true ? [] : void 0), 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;
};
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;
const q = Array.isArray(s.quantization) ? s.quantization : [];
el.quantDropdownMenu.querySelectorAll("input").forEach((i) => i.checked = q.includes(i.value));
el.quantDropdownBtn.textContent = q.length ? q.join(", ") : "Any";
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_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 === "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.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.quantization = [...el.quantDropdownMenu.querySelectorAll("input:checked")].map((i) => i.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.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);
}
const 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())}`;
};
let 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.threadsExportOption).on("click", () => {
dl(`threads-${ts()}.json`, { version: 1, threads: THREAD.list });
el.userMenu.classList.add("hidden");
});
$(el.threadsImportOption).on("click", () => {
importMode = "threads";
el.importInput.value = "";
el.importInput.click();
});
$(el.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") {
const arr = Array.isArray(data) ? data : Array.isArray(data.threads) ? data.threads : [];
if (!arr.length) throw new Error("No threads");
const norm = (t) => ({ id: t.id || gid(), title: titleFrom(t.title || titleFrom(t.messages?.find?.((m) => m.role === "user")?.content || "")), pinned: !!t.pinned, updatedAt: t.updatedAt || Date.now(), messages: Array.isArray(t.messages) ? t.messages.filter((m) => m && m.role && m.content) : [] });
const best = {};
arr.forEach((t) => {
const n = norm(t), k = n.id, prev = best[k];
best[k] = !prev || +n.updatedAt > +prev.updatedAt ? n : prev;
});
let kept = 0, skipped = 0;
const idx = Object.fromEntries(THREAD.list.map((t) => [t.id, t]));
for (const th of Object.values(best)) {
const ex = idx[th.id];
if (ex && +ex.updatedAt >= +th.updatedAt) {
skipped++;
continue;
}
if (!ex) THREAD.list.push(th);
else Object.assign(ex, th);
kept++;
}
await THREAD.save();
await renderThreads();
alert(`${kept} imported, ${skipped} skipped (older).`);
}
el.userMenu.classList.add("hidden");
} catch {
alert("Import failed");
} finally {
importMode = null;
}
});
function kbUpdate() {
const vv = window.visualViewport;
const overlap = vv ? Math.max(0, window.innerHeight - (vv.height + vv.offsetTop)) : 0;
document.documentElement.style.setProperty("--kb", overlap + "px");
const fh = el.footer.getBoundingClientRect().height;
document.documentElement.style.setProperty("--footer-h", fh + "px");
el.footer.style.transform = "translateY(" + -overlap + "px)";
el.chat.style.scrollPaddingBottom = fh + overlap + 16 + "px";
}
function 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 };
}
const USER = window.USER = { log: async (s) => {
const t = String(s ?? "").trim();
if (!t) return;
await ensureThreadOnFirstUser();
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.content));
});
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", "cloudflare"].includes(v) ? v : "openrouter");
}, get apiKeyOpenRouter() {
return localStorage.getItem("openrouter_api_key") || DEFAULT_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 donor() {
return localStorage.getItem("user_donor") !== "false";
}, set donor(v) {
localStorage.setItem("user_donor", String(!!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() {
await SUNE.fetchDotSune("sune-org/store@main/marketplace.sune");
await THREAD.load();
await renderThreads();
renderSidebar();
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");
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_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);
el.set_donor.checked = USER.donor;
const updateProv = () => {
const d = el.set_donor.checked;
Array.from(el.set_provider.options).forEach((o) => {
if (o.value !== "openrouter") {
o.disabled = !d;
if (!d) o.hidden = true;
else o.hidden = false;
}
});
if (!d && el.set_provider.value !== "openrouter") el.set_provider.value = "openrouter";
};
updateProv();
el.set_donor.onchange = updateProv;
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();
USER.donor = el.set_donor.checked;
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");
const m = { provider: "provider", apiKeyOpenRouter: "apiKeyOR", apiKeyOpenAI: "apiKeyOAI", apiKeyGoogle: "apiKeyG", apiKeyClaude: "apiKeyC", apiKeyCloudflare: "apiKeyCF", masterPrompt: "masterPrompt", titleModel: "titleModel", githubToken: "ghToken", name: "userName", avatar: "userAvatar", gcpSA: "gcpSA" };
Object.entries(m).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");
}
};
const getBubbleById = (id) => el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`);
async function syncActiveThread() {
if (!USER.donor) return false;
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 prevText = bubble.textContent || "";
const j = await fetch(HTTP_BASE + "?uid=" + encodeURIComponent(id)).then((r) => r.ok ? r.json() : null).catch(() => null);
const finalise = (t, c) => {
renderMarkdown(bubble, t, { enhance: false });
enhanceCodeBlocks(bubble, true);
const i = state.messages.findIndex((x) => x.id === id);
if (i >= 0) state.messages[i].content = c;
else state.messages.push({ id, role: "assistant", content: c, ...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 = prevText + "\n\n" + j.error;
finalise(t, [{ type: "text", text: t }]);
}
return false;
}
const text = j.text || "", isDone = j.error || j.done || j.phase === "done";
if (text) renderMarkdown(bubble, text, { enhance: false });
if (isDone) {
const finalText = text || prevText;
finalise(finalText, [{ type: "text", text: finalText }]);
return false;
}
await cacheStore.setItem(id, "busy");
return true;
}
let syncLoopRunning = false;
async function syncWhileBusy() {
if (syncLoopRunning || document.visibilityState === "hidden") return;
syncLoopRunning = true;
try {
while (await syncActiveThread()) await new Promise((r) => setTimeout(r, 1500));
} finally {
syncLoopRunning = false;
}
}
const onForeground = () => {
if (document.visibilityState !== "visible") return;
state.controller?.disconnect?.();
if (state.busy) syncWhileBusy();
};
$(document).on("visibilitychange", onForeground);
$(el.copySystemPrompt).on("click", async () => {
try {
await navigator.clipboard.writeText(el.set_system_prompt.value || "");
} catch {
}
});
$(el.pasteSystemPrompt).on("click", async () => {
try {
el.set_system_prompt.value = await navigator.clipboard.readText();
} catch {
}
});
const 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 {
}
});
$(el.quantDropdownBtn).on("click", (e) => {
e.stopPropagation();
el.quantDropdownMenu.classList.toggle("hidden");
});
$(el.quantDropdownMenu).on("click", (e) => e.stopPropagation()).on("change", "input", () => {
const q = [...el.quantDropdownMenu.querySelectorAll("input:checked")].map((i) => i.value);
el.quantDropdownBtn.textContent = q.length ? q.join(", ") : "Any";
});
$(window).on("click", () => el.quantDropdownMenu.classList.add("hidden"));
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, 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 });