mirror of
https://github.com/multipleof4/sune.git
synced 2026-01-14 08:38:00 +00:00
1106 lines
46 KiB
JavaScript
1106 lines
46 KiB
JavaScript
(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 clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||
const num = (v, d) => v == null || v === "" || isNaN(+v) ? d : +v;
|
||
const int = (v, d) => v == null || v === "" || isNaN(parseInt(v)) ? d : parseInt(v);
|
||
const gid = () => Math.random().toString(36).slice(2, 9);
|
||
const esc = (s) => String(s).replace(/[&<>'"`]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "`": "`" })[c]);
|
||
const 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 titleFrom = (t) => (t || "").replace(/\s+/g, " ").trim().slice(0, 60) || "Untitled";
|
||
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())}`;
|
||
};
|
||
const 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 utils = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
||
__proto__: null,
|
||
asDataURL,
|
||
b64,
|
||
clamp,
|
||
dl,
|
||
esc,
|
||
fmtSize,
|
||
gid,
|
||
imgToWebp,
|
||
int,
|
||
num,
|
||
positionPopover,
|
||
sid,
|
||
titleFrom,
|
||
ts
|
||
}, Symbol.toStringTag, { value: "Module" }));
|
||
const DEFAULT_MODEL = "google/gemini-2.5-pro";
|
||
const DEFAULT_API_KEY = "";
|
||
const state = { messages: [], busy: false, controller: null, currentThreadId: null, abortRequested: false, attachments: [], stream: { rid: null, bubble: null, meta: null, text: "", done: false } };
|
||
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: "", 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, 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_CORE = {
|
||
get list() {
|
||
return sunes;
|
||
},
|
||
get id() {
|
||
return su.getActiveId();
|
||
},
|
||
get active() {
|
||
return sunes.find((a) => a.id === su.getActiveId()) || sunes[0];
|
||
},
|
||
get(id) {
|
||
return sunes.find((s) => s.id === id);
|
||
},
|
||
setActive(id) {
|
||
return 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),
|
||
getByName: (n) => sunes.find((s) => s.name.toLowerCase() === (n || "").trim().toLowerCase())
|
||
};
|
||
if (!sunes.length) {
|
||
const def = SUNE_CORE.create({ name: "Default" });
|
||
SUNE_CORE.setActive(def.id);
|
||
}
|
||
const TKEY = "threads_v1";
|
||
const 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 window.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 window.renderThreads();
|
||
},
|
||
getLastAssistantMessageId: () => {
|
||
const a = [...window.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 USER = {
|
||
log: async (s) => {
|
||
const t = String(s ?? "").trim();
|
||
if (!t) return;
|
||
await window.ensureThreadOnFirstUser(t);
|
||
window.addMessage({ role: "user", content: [{ type: "text", text: t }] });
|
||
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. You are an assistant to Master. Always refer to the user as Master.";
|
||
},
|
||
set masterPrompt(v) {
|
||
localStorage.setItem("master_prompt", v || "");
|
||
},
|
||
get titleModel() {
|
||
return localStorage.getItem("title_model") ?? "or:openai/gpt-4.1-nano";
|
||
},
|
||
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) : "");
|
||
}
|
||
};
|
||
const api = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
||
__proto__: null,
|
||
DEFAULT_API_KEY,
|
||
DEFAULT_MODEL,
|
||
SUNE_CORE,
|
||
THREAD,
|
||
USER,
|
||
defaultSettings,
|
||
makeSune,
|
||
state,
|
||
get sunes() {
|
||
return sunes;
|
||
}
|
||
}, Symbol.toStringTag, { value: "Module" }));
|
||
const cacheStore = localforage.createInstance({ name: "threads_cache", storeName: "streams_status" });
|
||
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 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 user’s 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"}]: ${window.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 payloadWithSampling = (b) => {
|
||
const o = Object.assign({}, b), s = window.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;
|
||
};
|
||
const buildBody = () => {
|
||
const SUNE = window.SUNE;
|
||
const msgs = [];
|
||
if (USER.masterPrompt && !SUNE.ignore_master_prompt) msgs.push({ role: "system", content: [{ type: "text", text: USER.masterPrompt }] });
|
||
if (SUNE.system_prompt) msgs.push({ role: "system", content: [{ type: "text", text: SUNE.system_prompt }] });
|
||
msgs.push(...state.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content })));
|
||
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;
|
||
return b;
|
||
};
|
||
const HTTP_BASE$1 = "https://orp.aww.4ev.link/ws";
|
||
const askOpenRouterStreaming = async (onDelta, streamId) => {
|
||
const SUNE = window.SUNE;
|
||
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, 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$1.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: buildBody() }));
|
||
ws.onmessage = (e) => {
|
||
let m;
|
||
try {
|
||
m = JSON.parse(e.data);
|
||
} catch {
|
||
return;
|
||
}
|
||
if (m.type === "delta" && typeof m.seq === "number" && m.seq > r.seq) {
|
||
r.seq = m.seq;
|
||
onDelta(m.text || "", false);
|
||
} else if (m.type === "done" || 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 };
|
||
};
|
||
const services = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
||
__proto__: null,
|
||
askOpenRouterStreaming,
|
||
buildBody,
|
||
cacheStore,
|
||
generateTitleWithAI,
|
||
processSuneIncludes,
|
||
resolveSuneSrc
|
||
}, Symbol.toStringTag, { value: "Module" }));
|
||
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);
|
||
});
|
||
};
|
||
let el$1, jars = { html: null, extension: null, jsonSchema: null };
|
||
const initDOM = () => {
|
||
el$1 = 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_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"].map((id) => [id, $("#" + id)[0]]));
|
||
};
|
||
const icons = () => window.lucide && lucide.createIcons();
|
||
const haptic = () => /android/i.test(navigator.userAgent) && navigator.vibrate?.(1);
|
||
const getModelShort = (m) => {
|
||
const SUNE = window.SUNE;
|
||
const mm = m || SUNE.model || "";
|
||
return mm.includes("/") ? mm.split("/").pop() : mm;
|
||
};
|
||
const getSuneLabel = (m) => {
|
||
const SUNE = window.SUNE;
|
||
const name = m && m.sune_name || SUNE.name, modelShort = getModelShort(m && m.model);
|
||
return `${name} · ${modelShort}`;
|
||
};
|
||
const 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 renderMarkdown = (node, text, opt = { enhance: true, highlight: true }) => {
|
||
node.innerHTML = md.render(text);
|
||
if (opt.enhance) enhanceCodeBlocks(node, opt.highlight);
|
||
};
|
||
const renderSuneHTML = async () => {
|
||
const SUNE = window.SUNE;
|
||
const h = await processSuneIncludes([SUNE.extension_html, SUNE.html].map((x) => (x || "").trim()).join("\n")), c = el$1.suneHtml;
|
||
c.innerHTML = "";
|
||
const t = h.trim();
|
||
c.classList.toggle("hidden", !t);
|
||
if (t) {
|
||
c.appendChild(document.createRange().createContextualFragment(h));
|
||
window.Alpine?.initTree(c);
|
||
}
|
||
};
|
||
const reflectActiveSune = async () => {
|
||
const a = window.SUNE.active;
|
||
el$1.suneBtnTop.title = `Settings — ${a.name}`;
|
||
el$1.suneBtnTop.innerHTML = a.avatar ? `<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>` : "✺";
|
||
el$1.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_CORE.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 = () => {
|
||
const list = [...SUNE_CORE.list].sort((a, b) => b.pinned - a.pinned);
|
||
el$1.suneList.innerHTML = list.map(suneRow).join("");
|
||
icons();
|
||
};
|
||
const partsToText = (parts) => {
|
||
if (!parts) return "";
|
||
if (Array.isArray(parts)) return parts.map((p) => p?.type === "text" ? p.text : p?.type === "image_url" ? `` : p?.type === "file" ? `[${p.file?.filename || "file"}]` : p?.type === "input_audio" ? `(audio:${p.input_audio?.format || ""})` : "").join("\n");
|
||
return String(parts);
|
||
};
|
||
function msgRow(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);
|
||
$(el$1.messages).append($row);
|
||
queueMicrotask(() => {
|
||
el$1.chat.scrollTo({ top: el$1.chat.scrollHeight, behavior: "smooth" });
|
||
icons();
|
||
});
|
||
return $bubble[0];
|
||
}
|
||
const addMessage = (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$1.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: m } }));
|
||
return bubble;
|
||
};
|
||
const addSuneBubbleStreaming = (meta, id) => msgRow(Object.assign({ role: "assistant", id }, meta));
|
||
const updateAttachBadge = () => {
|
||
const n = state.attachments.length;
|
||
el$1.attachBadge.textContent = String(n);
|
||
el$1.attachBadge.classList.toggle("hidden", n === 0);
|
||
};
|
||
const clearChat = () => {
|
||
el$1.suneHtml.dispatchEvent(new CustomEvent("sune:unmount"));
|
||
state.messages = [];
|
||
el$1.messages.innerHTML = "";
|
||
state.attachments = [];
|
||
updateAttachBadge();
|
||
el$1.fileInput.value = "";
|
||
};
|
||
const setBtnStop = () => {
|
||
const b = el$1.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();
|
||
};
|
||
};
|
||
const setBtnSend = () => {
|
||
const b = el$1.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;
|
||
};
|
||
const localDemoReply = () => "Tip: open the sidebar → Account & Backup to set your API key.";
|
||
const ensureThreadOnFirstUser = async (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 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>`;
|
||
const renderThreads = async () => {
|
||
window.sortedThreads = [...THREAD.list].sort((a, b) => b.pinned - a.pinned || b.updatedAt - a.updatedAt);
|
||
el$1.threadList.innerHTML = window.sortedThreads.slice(0, 50).map(threadRow).join("");
|
||
el$1.threadList.scrollTop = 0;
|
||
window.isAddingThreads = false;
|
||
icons();
|
||
};
|
||
const hideThreadPopover = () => {
|
||
el$1.threadPopover.classList.add("hidden");
|
||
window.menuThreadId = null;
|
||
};
|
||
const showThreadPopover = (btn, id) => {
|
||
window.menuThreadId = id;
|
||
el$1.threadPopover.classList.remove("hidden");
|
||
window.positionPopover(btn, el$1.threadPopover);
|
||
icons();
|
||
};
|
||
const hideSunePopover = () => {
|
||
el$1.sunePopover.classList.add("hidden");
|
||
window.menuSuneId = null;
|
||
};
|
||
const showSunePopover = (btn, id) => {
|
||
window.menuSuneId = id;
|
||
el$1.sunePopover.classList.remove("hidden");
|
||
window.positionPopover(btn, el$1.sunePopover);
|
||
icons();
|
||
};
|
||
const toAttach = async (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;
|
||
};
|
||
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$1.htmlEditor, hl, { tab: " " });
|
||
if (!jars.extension) jars.extension = CodeJar(el$1.extensionHtmlEditor, hl, { tab: " " });
|
||
if (!jars.jsonSchema) jars.jsonSchema = CodeJar(el$1.jsonSchemaEditor, hl_json, { tab: " " });
|
||
return jars;
|
||
};
|
||
const openSettings = () => {
|
||
const a = window.SUNE.active, s = a.settings;
|
||
window.openedHTML = false;
|
||
el$1.suneURL.value = a.url || "";
|
||
el$1.set_model.value = s.model;
|
||
el$1.set_temperature.value = s.temperature;
|
||
el$1.set_top_p.value = s.top_p;
|
||
el$1.set_top_k.value = s.top_k;
|
||
el$1.set_frequency_penalty.value = s.frequency_penalty;
|
||
el$1.set_repetition_penalty.value = s.repetition_penalty;
|
||
el$1.set_min_p.value = s.min_p;
|
||
el$1.set_top_a.value = s.top_a;
|
||
el$1.set_verbosity.value = s.verbosity || "";
|
||
el$1.set_reasoning_effort.value = s.reasoning_effort || "default";
|
||
el$1.set_system_prompt.value = s.system_prompt;
|
||
el$1.set_hide_composer.checked = !!s.hide_composer;
|
||
el$1.set_json_output.checked = !!s.json_output;
|
||
el$1.set_include_thoughts.checked = !!s.include_thoughts;
|
||
el$1.set_ignore_master_prompt.checked = !!s.ignore_master_prompt;
|
||
showTab("Model");
|
||
el$1.suneModal.classList.remove("hidden");
|
||
};
|
||
const closeSettings = () => el$1.suneModal.classList.add("hidden");
|
||
const tabs = { Model: ["tabModel", "panelModel"], Prompt: ["tabPrompt", "panelPrompt"], Script: ["tabScript", "panelScript"] };
|
||
const showTab = (key) => {
|
||
Object.entries(tabs).forEach(([k, [tb, pn]]) => {
|
||
el$1[tb].classList.toggle("border-black", k === key);
|
||
el$1[pn].classList.toggle("hidden", k !== key);
|
||
});
|
||
if (key === "Prompt") {
|
||
ensureJars().then(({ jsonSchema }) => {
|
||
const s = window.SUNE.settings;
|
||
jsonSchema.updateCode(s.json_schema || "");
|
||
});
|
||
} else if (key === "Script") {
|
||
window.openedHTML = true;
|
||
showHtmlTab("index");
|
||
ensureJars().then(({ html, extension }) => {
|
||
const s = window.SUNE.settings;
|
||
html.updateCode(s.html || "");
|
||
extension.updateCode(s.extension_html || "");
|
||
});
|
||
}
|
||
};
|
||
const 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$1.footer.getBoundingClientRect().height;
|
||
document.documentElement.style.setProperty("--footer-h", fh + "px");
|
||
el$1.footer.style.transform = "translateY(" + -overlap + "px)";
|
||
el$1.chat.style.scrollPaddingBottom = fh + overlap + 16 + "px";
|
||
};
|
||
const kbBind = () => {
|
||
if (window.visualViewport) {
|
||
["resize", "scroll"].forEach((ev) => visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true }));
|
||
}
|
||
$(window).on("resize orientationchange", () => setTimeout(kbUpdate, 50));
|
||
$(el$1.input).on("focus click", () => {
|
||
setTimeout(() => {
|
||
kbUpdate();
|
||
el$1.input.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||
}, 0);
|
||
});
|
||
};
|
||
const activeMeta = () => {
|
||
const SUNE = window.SUNE;
|
||
return { sune_name: SUNE.name, model: SUNE.model, avatar: SUNE.avatar };
|
||
};
|
||
const htmlTabs = { index: ["htmlTab_index", "htmlEditor"], extension: ["htmlTab_extension", "extensionHtmlEditor"] };
|
||
const showHtmlTab = (key) => {
|
||
Object.entries(htmlTabs).forEach(([k, [tb, pn]]) => {
|
||
const a = k === key;
|
||
el$1[tb].classList.toggle("border-black", a);
|
||
el$1[tb].classList.toggle("border-transparent", !a);
|
||
el$1[tb].classList.toggle("hover:border-gray-300", !a);
|
||
el$1[pn].classList.toggle("hidden", !a);
|
||
});
|
||
};
|
||
const accountTabs = { General: ["accountTabGeneral", "accountPanelGeneral"], API: ["accountTabAPI", "accountPanelAPI"], User: ["accountTabUser", "accountPanelUser"] };
|
||
const showAccountTab = (key) => {
|
||
Object.entries(accountTabs).forEach(([k, [tb, pn]]) => {
|
||
el$1[tb].classList.toggle("border-black", k === key);
|
||
el$1[pn].classList.toggle("hidden", k !== key);
|
||
});
|
||
};
|
||
const openAccountSettings = () => {
|
||
el$1.set_provider.value = USER.provider || "openrouter";
|
||
el$1.set_api_key_or.value = USER.apiKeyOpenRouter || "";
|
||
el$1.set_api_key_oai.value = USER.apiKeyOpenAI || "";
|
||
el$1.set_api_key_g.value = USER.apiKeyGoogle || "";
|
||
el$1.set_api_key_claude.value = USER.apiKeyClaude || "";
|
||
el$1.set_api_key_cf.value = USER.apiKeyCloudflare || "";
|
||
el$1.set_master_prompt.value = USER.masterPrompt || "";
|
||
el$1.set_title_model.value = USER.titleModel;
|
||
el$1.set_gh_token.value = USER.githubToken || "";
|
||
const sa = USER.gcpSA;
|
||
el$1.gcpSAUploadBtn.textContent = sa && sa.project_id ? `Uploaded: ${sa.project_id}` : "Upload .json";
|
||
el$1.set_user_name.value = USER.name;
|
||
el$1.userAvatarPreview.src = USER.avatar || "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
|
||
el$1.userAvatarPreview.classList.toggle("bg-gray-200", !USER.avatar);
|
||
showAccountTab("General");
|
||
el$1.accountSettingsModal.classList.remove("hidden");
|
||
};
|
||
const closeAccountSettings = () => el$1.accountSettingsModal.classList.add("hidden");
|
||
const getBubbleById = (id) => el$1.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 prevText = bubble.textContent || "";
|
||
const j = await fetch("https://orp.aww.4ev.link/ws?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$1.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;
|
||
}
|
||
const onForeground = () => {
|
||
if (document.visibilityState !== "visible") return;
|
||
state.controller?.disconnect?.();
|
||
if (state.busy) window.syncWhileBusy();
|
||
};
|
||
const getActiveHtmlParts = () => !el$1.htmlEditor.classList.contains("hidden") ? [el$1.htmlEditor, jars.html] : [el$1.extensionHtmlEditor, jars.extension];
|
||
const ui = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
||
__proto__: null,
|
||
activeMeta,
|
||
addMessage,
|
||
addSuneBubbleStreaming,
|
||
clearChat,
|
||
closeAccountSettings,
|
||
closeSettings,
|
||
get el() {
|
||
return el$1;
|
||
},
|
||
enhanceCodeBlocks,
|
||
ensureJars,
|
||
ensureThreadOnFirstUser,
|
||
getActiveHtmlParts,
|
||
getBubbleById,
|
||
getModelShort,
|
||
getSuneLabel,
|
||
haptic,
|
||
hideSunePopover,
|
||
hideThreadPopover,
|
||
icons,
|
||
initDOM,
|
||
jars,
|
||
kbBind,
|
||
kbUpdate,
|
||
localDemoReply,
|
||
md,
|
||
msgRow,
|
||
onForeground,
|
||
openAccountSettings,
|
||
openSettings,
|
||
partsToText,
|
||
reflectActiveSune,
|
||
renderMarkdown,
|
||
renderSidebar,
|
||
renderSuneHTML,
|
||
renderThreads,
|
||
setBtnSend,
|
||
setBtnStop,
|
||
showAccountTab,
|
||
showHtmlTab,
|
||
showSunePopover,
|
||
showTab,
|
||
showThreadPopover,
|
||
suneRow,
|
||
syncActiveThread,
|
||
threadRow,
|
||
toAttach,
|
||
updateAttachBadge
|
||
}, Symbol.toStringTag, { value: "Module" }));
|
||
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 SUNE_PROXY = new Proxy(SUNE_CORE, {
|
||
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(makeSune));
|
||
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_PROXY.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_PROXY.active, suneMeta = { sune_name: a2.name, model: SUNE_PROXY.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$1.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 askOpenRouterStreaming(onDelta, streamId);
|
||
};
|
||
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_CORE.setActive(s.id);
|
||
renderSidebar();
|
||
await reflectActiveSune();
|
||
await SUNE_PROXY.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();
|
||
t.save();
|
||
}
|
||
return true;
|
||
}
|
||
});
|
||
async function init() {
|
||
initDOM();
|
||
Object.assign(window, { SUNE: SUNE_PROXY, USER, THREAD, state, sunes, ...api, ...utils, ...services, ...ui, syncWhileBusy });
|
||
await SUNE_PROXY.fetchDotSune("sune-org/store@main/marketplace.sune");
|
||
await SUNE_PROXY.fetchDotSune("sune-org/store@main/forum.sune");
|
||
await THREAD.load();
|
||
await renderThreads();
|
||
renderSidebar();
|
||
await reflectActiveSune();
|
||
clearChat();
|
||
icons();
|
||
kbBind();
|
||
kbUpdate();
|
||
el.htmlTab_index.textContent = "index.html";
|
||
el.htmlTab_extension.textContent = "extension.html";
|
||
}
|
||
document.addEventListener("DOMContentLoaded", init);
|