(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, ...m.images ? { images: m.images } : {} }))); 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 (SUNE2.verbosity) b.verbosity = SUNE2.verbosity; if (SUNE2.img_output) { b.modalities = ["text", "image"]; b.image_config = { aspect_ratio: SUNE2.aspect_ratio || "1:1", image_size: SUNE2.image_size || "1K" }; } 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]") { onDelta("", true); 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) onDelta("", false, imgs); } 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, m.images); } 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", "set_verbosity", "set_reasoning_effort", "set_system_prompt", "set_hide_composer", "set_include_thoughts", "set_json_output", "set_img_output", "set_aspect_ratio", "set_image_size", "aspectRatioContainer", "set_ignore_master_prompt", "deleteSuneBtn", "sidebarLeft", "sidebarOverlayLeft", "sidebarBtnLeft", "suneList", "newSuneBtn", "userMenuBtn", "userMenu", "accountSettingsOption", "sunesImportOption", "sunesExportOption", "threadsImportOption", "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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "`": "`" })[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: "", verbosity: "", reasoning_effort: "default", system_prompt: "", html: "", extension_html: "", hide_composer: false, include_thoughts: false, json_output: false, img_output: false, aspect_ratio: "1:1", image_size: "1K", ignore_master_prompt: false, json_schema: "" }; 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: null, bubble: null, meta: null, text: "", done: false }; let buf = "", completed = false; const onDelta = (delta, done, imgs) => { if (imgs) { if (!assistantMsg.images) assistantMsg.images = []; assistantMsg.images.push(...imgs); } buf += delta; state.stream.text = buf; renderMarkdown(suneBubble, partsToText(assistantMsg), { enhance: false }); assistantMsg.content[0].text = buf; if (done && !completed) { completed = true; setBtnSend(); state.busy = false; enhanceCodeBlocks(suneBubble, true); THREAD.persist(true); el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: assistantMsg } })); state.stream = { rid: null, bubble: null, meta: null, text: "", done: false }; } else if (!done) THREAD.persist(false); }; await streamChat(onDelta, streamId); }; if (p === "getByName") return (n) => sunes.find((s) => s.name.toLowerCase() === (n || "").trim().toLowerCase()); if (p === "handoff") return async (n) => { await new Promise((r) => setTimeout(r, 4e3)); const s = sunes.find((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 ""; 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 ? `` : "✺"; el.footer.classList.toggle("hidden", !!a.settings.hide_composer); await renderSuneHTML(); icons(); }; const suneRow = (a) => `
`; 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 = $('').on("click", async (e) => { e.stopPropagation(); try { await navigator.clipboard.writeText(code.innerText); $btn.text("Copied"); setTimeout(() => $btn.text("Copy"), 1200); } catch { } }); const $container = $('
'); $container.append($(`${countText} chars`), $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 = $('
'), $head = $('
'), $avatar = $("
"); const uAva = isUser ? USER.avatar : meta.avatar; uAva ? $avatar.attr("class", "msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden").html(``) : $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 = $('
').text(isUser ? USER.name : getSuneLabel(meta)); const $deleteBtn = $('').on("click", async (e) => { e.stopPropagation(); state.messages = state.messages.filter((msg) => msg.id !== m.id); $row.remove(); await THREAD.persist(); }); const $copyBtn = $('').on("click", async function(e) { e.stopPropagation(); try { await navigator.clipboard.writeText(partsToText(m)); $(this).html(''); icons(); setTimeout(() => { $(this).html(''); icons(); }, 1200); } catch { } }); $head.append($avatar, $name, $copyBtn, $deleteBtn); const $bubble = $(`
`); $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(m) { if (!m) return ""; const c = m.content, i = m.images; let t = Array.isArray(c) ? c.map((p) => p?.type === "text" ? p.text : p?.type === "image_url" ? `![](${p.image_url?.url || ""})` : p?.type === "file" ? `[${p.file?.filename || "file"}]` : p?.type === "input_audio" ? `(audio:${p.input_audio?.format || ""})` : "").join("\n") : String(c || ""); if (Array.isArray(i)) t += i.map((x) => ` ![](${x.image_url?.url}) `).join(""); return t; } const addMessage = window.addMessage = function(m, track = true) { m.id = m.id || gid(); if (!Array.isArray(m.content) && m.content != null) { m.content = [{ type: "text", text: String(m.content) }]; } const bubble = msgRow(m); bubble.dataset.mid = m.id; renderMarkdown(bubble, partsToText(m)); if (track) state.messages.push(m); if (m.role === "assistant") el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: m } })); return bubble; }; const addSuneBubbleStreaming = (meta, id) => msgRow(Object.assign({ role: "assistant", id }, meta)); const clearChat = () => { el.suneHtml.dispatchEvent(new CustomEvent("sune:unmount")); state.messages = []; el.messages.innerHTML = ""; state.attachments = []; updateAttachBadge(); el.fileInput.value = ""; }; const payloadWithSampling = (b) => { const o = Object.assign({}, b), s = SUNE, p = { temperature: num(s.temperature, null), top_p: num(s.top_p, null), top_k: int(s.top_k, null), frequency_penalty: num(s.frequency_penalty, null), repetition_penalty: num(s.repetition_penalty, null), min_p: num(s.min_p, null), top_a: num(s.top_a, null) }; 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 = ''; 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 = ''; 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 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"}]: ${partsToText(m)}`).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) => `
`; 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)); } 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) || "").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")) || "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, imgs) => { if (imgs) { if (!assistantMsg.images) assistantMsg.images = []; assistantMsg.images.push(...imgs); } buf += delta; state.stream.text = buf; renderMarkdown(suneBubble, partsToText(assistantMsg), { enhance: false }); assistantMsg.content[0].text = buf; if (done && !completed) { completed = true; setBtnSend(); state.busy = false; enhanceCodeBlocks(suneBubble, true); THREAD.persist(true); el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: assistantMsg } })); state.stream = { rid: null, bubble: null, meta: null, text: "", done: false }; } else if (!done) THREAD.persist(false); }; await streamChat(onDelta, streamId); state.attachments = []; updateAttachBadge(); }); 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; el.set_verbosity.value = s.verbosity || ""; el.set_reasoning_effort.value = s.reasoning_effort || "default"; el.set_system_prompt.value = s.system_prompt; el.set_hide_composer.checked = !!s.hide_composer; el.set_json_output.checked = !!s.json_output; el.set_img_output.checked = !!s.img_output; el.set_aspect_ratio.value = s.aspect_ratio || "1:1"; el.set_image_size.value = s.image_size || "1K"; el.aspectRatioContainer.classList.toggle("hidden", !s.img_output); el.set_include_thoughts.checked = !!s.include_thoughts; el.set_ignore_master_prompt.checked = !!s.ignore_master_prompt; showTab("Model"); el.suneModal.classList.remove("hidden"); } 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.set_img_output).on("change", (e) => el.aspectRatioContainer.classList.toggle("hidden", !e.target.checked)); $(el.settingsForm).on("submit", async (e) => { e.preventDefault(); SUNE.url = (el.suneURL.value || "").trim(); SUNE.model = (el.set_model.value || "").trim(); ["temperature", "top_p", "top_k", "frequency_penalty", "repetition_penalty", "min_p", "top_a"].forEach((k) => SUNE[k] = el[`set_${k}`].value.trim()); SUNE.verbosity = el.set_verbosity.value || ""; SUNE.reasoning_effort = el.set_reasoning_effort.value || "default"; SUNE.system_prompt = el.set_system_prompt.value.trim(); SUNE.hide_composer = el.set_hide_composer.checked; SUNE.json_output = el.set_json_output.checked; SUNE.img_output = el.set_img_output.checked; SUNE.aspect_ratio = el.set_aspect_ratio.value; SUNE.image_size = el.set_image_size.value; SUNE.include_thoughts = el.set_include_thoughts.checked; SUNE.ignore_master_prompt = el.set_ignore_master_prompt.checked; SUNE.json_schema = el.jsonSchemaEditor.textContent; if (openedHTML) { SUNE.html = el.htmlEditor.textContent; SUNE.extension_html = el.extensionHtmlEditor.textContent; } closeSettings(); await reflectActiveSune(); }); $(el.deleteSuneBtn).on("click", async () => { const activeId = SUNE.id, name = SUNE.name || "this sune"; if (!confirm(`Delete "${name}"?`)) return; SUNE.delete(activeId); renderSidebar(); await reflectActiveSune(); state.currentThreadId = null; clearChat(); closeSettings(); }); $(el.newSuneBtn).on("click", async () => { const name = prompt("Name your sune:"); if (!name) return; const sune = SUNE.create({ name: name.trim() }); SUNE.setActive(sune.id); renderSidebar(); await reflectActiveSune(); state.currentThreadId = null; clearChat(); document.getElementById("sidebarLeft").classList.add("-translate-x-full"); document.getElementById("sidebarOverlayLeft").classList.add("hidden"); }); function dl(name, obj) { const blob = new Blob([JSON.stringify(obj, null, 2)], { type: name.endsWith(".sune") ? "application/octet-stream" : "application/json" }), url = URL.createObjectURL(blob), a = $("").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)); }); el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: "smooth" }); icons(); }); await THREAD.persist(); }, get PAT() { return this.githubToken; }, get name() { return localStorage.getItem("user_name") || "Anon"; }, set name(v) { localStorage.setItem("user_name", v || ""); }, get avatar() { return localStorage.getItem("user_avatar") || ""; }, set avatar(v) { localStorage.setItem("user_avatar", v || ""); }, get provider() { return localStorage.getItem("provider") || "openrouter"; }, set provider(v) { localStorage.setItem("provider", ["openai", "google", "claude"].includes(v) ? v : "openrouter"); }, get apiKeyOpenRouter() { return localStorage.getItem("openrouter_api_key") || 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, imgs) => { const tempMsg = { content: c, images: imgs }; renderMarkdown(bubble, partsToText(tempMsg), { enhance: false }); enhanceCodeBlocks(bubble, true); const i = state.messages.findIndex((x) => x.id === id); if (i >= 0) { state.messages[i].content = c; state.messages[i].images = imgs; } else state.messages.push({ id, role: "assistant", content: c, images: imgs, ...activeMeta() }); THREAD.persist(); setBtnSend(); state.busy = false; cacheStore.setItem(id, "done"); state.controller = null; el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: state.messages.find((m) => m.id === id) } })); }; if (!j || j.rid !== id) { if (j && j.error) { const t = 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"; const display = partsToText({ content: [{ type: "text", text }], images: j.images }); if (display) renderMarkdown(bubble, display, { enhance: false }); if (isDone) { const finalText = text || prevText; finalise(finalText, [{ type: "text", text: finalText }], j.images); 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 { } }); 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 });