diff --git a/dist/assets/index-pr-7k4fx.js b/dist/assets/index-CU6EhWVp.js similarity index 57% rename from dist/assets/index-pr-7k4fx.js rename to dist/assets/index-CU6EhWVp.js index 2739aaa..c51c858 100644 --- a/dist/assets/index-pr-7k4fx.js +++ b/dist/assets/index-CU6EhWVp.js @@ -27,12 +27,81 @@ 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 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 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-2.5-pro", 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_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 esc = (s) => String(s).replace(/[&<>'"`]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", "`": "`" })[c]); const fmtSize = (b) => { const u = ["B", "KB", "MB", "GB", "TB"]; let i = 0, x = b; @@ -47,23 +116,6 @@ const asDataURL = (f) => new Promise((r) => { fr.onload = () => r(String(fr.result || "")); fr.readAsDataURL(f); }); -const b64 = (x) => x.split(",")[1] || ""; -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 = $("").prop({ href: url, download: name }).appendTo("body"); - a.get(0).click(); - a.remove(); - URL.revokeObjectURL(url); -}; -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 titleFrom = (t) => (t || "").replace(/\s+/g, " ").trim().slice(0, 60) || "Untitled"; const imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => { if (!f) return j(); const i = new Image(); @@ -80,30 +132,7 @@ const imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => { i.onerror = j; i.src = URL.createObjectURL(f); }); -const haptic = () => /android/i.test(navigator.userAgent) && navigator.vibrate?.(1); -const icons = () => window.lucide && lucide.createIcons(); -const utils = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - asDataURL, - b64, - clamp, - dl, - esc, - fmtSize, - gid, - haptic, - icons, - imgToWebp, - int, - num, - positionPopover, - sid, - titleFrom, - ts -}, Symbol.toStringTag, { value: "Module" })); -const DEFAULT_MODEL = "google/gemini-2.5-pro", DEFAULT_API_KEY = ""; -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, 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 || {} }); +const b64 = (x) => x.split(",")[1] || ""; const su = { key: "sunes_v1", activeKey: "active_sune_id", load() { try { return JSON.parse(localStorage.getItem(this.key) || "[]"); @@ -117,8 +146,10 @@ const su = { key: "sunes_v1", activeKey: "active_sune_id", load() { }, 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, 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 = new Proxy({ get list() { +const SUNE = window.SUNE = new Proxy({ get list() { return sunes; }, get id() { return su.getActiveId(); @@ -154,51 +185,51 @@ const SUNE = new Proxy({ get list() { for (const f of files || []) arr.push(await toAttach(f)); const clean = arr.filter(Boolean); if (!clean.length) return; - await window.ensureThreadOnFirstUser("(attachments)"); - window.addMessage({ role: "assistant", content: clean, ...window.activeMeta() }); + 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 window.ensureThreadOnFirstUser(t2); - window.addMessage({ role: "assistant", content: [{ type: "text", text: t2 }], ...window.activeMeta() }); + await ensureThreadOnFirstUser(); + addMessage({ role: "assistant", content: [{ type: "text", text: t2 }], ...activeMeta() }); await THREAD.persist(); }; - if (p === "lastReply") return [...window.state.messages].reverse().find((m) => m.role === "assistant"); + if (p === "lastReply") return [...state.messages].reverse().find((m) => m.role === "assistant"); if (p === "infer") return async () => { - if (window.state.busy || !SUNE.model || window.state.abortRequested) { - window.state.abortRequested = false; + if (state.busy || !SUNE.model || state.abortRequested) { + state.abortRequested = false; return; } - await window.ensureThreadOnFirstUser("Sune Inference"); + await ensureThreadOnFirstUser(); const th = THREAD.active; - if (th && !th.title) (async () => THREAD.setTitle(th.id, await window.generateTitleWithAI(window.state.messages) || "Sune Inference"))(); - window.state.busy = true; - window.setBtnStop(); - const a2 = SUNE.active, suneMeta = { sune_name: a2.name, model: SUNE.model, avatar: a2.avatar || "" }, streamId = window.sid(), suneBubble = window.addSuneBubbleStreaming(suneMeta, streamId); + 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); - window.state.messages.push(assistantMsg); + state.messages.push(assistantMsg); THREAD.persist(false); - window.state.stream = { rid: streamId, bubble: suneBubble, meta: suneMeta, text: "", done: false }; + state.stream = { rid: streamId, bubble: suneBubble, meta: suneMeta, text: "", done: false }; let buf = "", completed = false; const onDelta = (delta, done) => { buf += delta; - window.state.stream.text = buf; - window.renderMarkdown(suneBubble, buf, { enhance: false }); + state.stream.text = buf; + renderMarkdown(suneBubble, buf, { enhance: false }); assistantMsg.content[0].text = buf; if (done && !completed) { completed = true; - window.setBtnSend(); - window.state.busy = false; - window.enhanceCodeBlocks(suneBubble, true); + setBtnSend(); + state.busy = false; + enhanceCodeBlocks(suneBubble, true); THREAD.persist(true); - window.el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: assistantMsg } })); - window.state.stream = { rid: null, bubble: null, meta: null, text: "", done: false }; + 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 window.askOpenRouterStreaming(onDelta, streamId); + await askOpenRouterStreaming(onDelta, streamId); }; if (p === "getByName") return (n) => sunes.find((s) => s.name.toLowerCase() === (n || "").trim().toLowerCase()); if (p === "handoff") return async (n) => { @@ -206,8 +237,8 @@ const SUNE = new Proxy({ get list() { const s = sunes.find((s2) => s2.name.toLowerCase() === (n || "").trim().toLowerCase()); if (!s) return; SUNE.setActive(s.id); - window.renderSidebar(); - await window.reflectActiveSune(); + renderSidebar(); + await reflectActiveSune(); await SUNE.infer(); }; if (p in t) return t[p]; @@ -236,34 +267,229 @@ if (!sunes.length) { const def = SUNE.create({ name: "Default" }); SUNE.setActive(def.id); } -const TKEY = "threads_v1"; -const THREAD = { list: [], load: async function() { +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.content)); + $(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(parts) { + if (!parts) return ""; + if (Array.isArray(parts)) return parts.map((p) => p?.type === "text" ? p.text : p?.type === "image_url" ? `![](${p.image_url?.url || ""})` : p?.type === "file" ? `[${p.file?.filename || "file"}]` : p?.type === "input_audio" ? `(audio:${p.input_audio?.format || ""})` : "").join("\n"); + return String(parts); +} +const addMessage = window.addMessage = function(m, track = true) { + m.id = m.id || gid(); + if (!Array.isArray(m.content) && m.content != null) { + m.content = [{ type: "text", text: String(m.content) }]; + } + const bubble = msgRow(m); + bubble.dataset.mid = m.id; + renderMarkdown(bubble, partsToText(m.content)); + if (track) state.messages.push(m); + if (m.role === "assistant") el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: m } })); + return bubble; +}; +const addSuneBubbleStreaming = (meta, id) => msgRow(Object.assign({ role: "assistant", id }, meta)); +const clearChat = () => { + el.suneHtml.dispatchEvent(new CustomEvent("sune:unmount")); + state.messages = []; + el.messages.innerHTML = ""; + state.attachments = []; + updateAttachBadge(); + el.fileInput.value = ""; +}; +const payloadWithSampling = (b) => { + const o = Object.assign({}, b), s = SUNE, p = { temperature: num(s.temperature, null), top_p: num(s.top_p, null), top_k: int(s.top_k, null), frequency_penalty: num(s.frequency_penalty, null), repetition_penalty: num(s.repetition_penalty, null), min_p: num(s.min_p, null), top_a: num(s.top_a, null) }; + Object.keys(p).forEach((k) => { + const v = p[k]; + if (v !== null) o[k] = v; + }); + return o; +}; +function setBtnStop() { + const b = el.sendBtn; + b.dataset.mode = "stop"; + b.type = "button"; + b.setAttribute("aria-label", "Stop"); + b.innerHTML = ''; + 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(window.state.currentThreadId); + return this.get(state.currentThreadId); }, persist: async function(full = true) { - if (!window.state.currentThreadId) return; + if (!state.currentThreadId) return; const th = this.active; if (!th) return; - th.messages = [...window.state.messages]; + th.messages = [...state.messages]; if (full) { th.updatedAt = Date.now(); } await this.save(); - if (full) await window.renderThreads(); + 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 window.renderThreads(); + await renderThreads(); }, getLastAssistantMessageId: () => { - const a = [...window.el.messages.querySelectorAll(".msg-bubble")]; + 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; @@ -271,33 +497,579 @@ const THREAD = { list: [], load: async function() { } return null; } }; -const USER = { log: async (s) => { +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.content)}`).join("\n\n"); + if (!convo) return null; + try { + const r = await fetch("https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: model.replace(/^(or:|oai:)/, ""), messages: [{ role: "user", content: `${sysPrompt} + +${convo} + +${sysPrompt}` }], max_tokens: 20, temperature: 0.2 }) }); + if (!r.ok) return null; + const d = await r.json(); + return (d.choices?.[0]?.message?.content?.trim() || "").replace(/["']/g, "") || null; + } catch (e) { + console.error("AI title gen failed:", e); + return null; + } +}; +const threadRow = (t) => `
`; +let sortedThreads = [], isAddingThreads = false; +const THREAD_PAGE_SIZE = 50; +async function renderThreads() { + sortedThreads = [...THREAD.list].sort((a, b) => b.pinned - a.pinned || b.updatedAt - a.updatedAt); + el.threadList.innerHTML = sortedThreads.slice(0, THREAD_PAGE_SIZE).map(threadRow).join(""); + el.threadList.scrollTop = 0; + isAddingThreads = false; + icons(); +} +let menuThreadId = null; +const hideThreadPopover = () => { + el.threadPopover.classList.add("hidden"); + menuThreadId = null; +}; +function showThreadPopover(btn, id) { + menuThreadId = id; + el.threadPopover.classList.remove("hidden"); + positionPopover(btn, el.threadPopover); + icons(); +} +let menuSuneId = null; +const hideSunePopover = () => { + el.sunePopover.classList.add("hidden"); + menuSuneId = null; +}; +function showSunePopover(btn, id) { + menuSuneId = id; + el.sunePopover.classList.remove("hidden"); + positionPopover(btn, el.sunePopover); + icons(); +} +$(el.threadList).on("click", async (e) => { + const openBtn = e.target.closest("[data-open-thread]"), menuBtn = e.target.closest("[data-thread-menu]"); + if (openBtn) { + const id = openBtn.getAttribute("data-open-thread"); + if (id !== state.currentThreadId && state.busy) { + state.controller?.disconnect?.(); + setBtnSend(); + state.busy = false; + state.controller = null; + } + const th = THREAD.get(id); + if (!th) return; + if (id === state.currentThreadId) { + el.sidebarRight.classList.add("translate-x-full"); + el.sidebarOverlayRight.classList.add("hidden"); + hideThreadPopover(); + return; + } + state.currentThreadId = id; + clearChat(); + state.messages = Array.isArray(th.messages) ? [...th.messages] : []; + for (const m of state.messages) { + const b = msgRow(m); + b.dataset.mid = m.id || ""; + renderMarkdown(b, partsToText(m.content)); + } + await renderSuneHTML(); + syncWhileBusy(); + queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: "smooth" })); + el.sidebarRight.classList.add("translate-x-full"); + el.sidebarOverlayRight.classList.add("hidden"); + hideThreadPopover(); + return; + } + if (menuBtn) { + e.stopPropagation(); + showThreadPopover(menuBtn, menuBtn.getAttribute("[data-thread-menu]") ? menuBtn.getAttribute("[data-thread-menu]") : menuBtn.getAttribute("data-thread-menu")); + } +}); +$(el.threadList).on("scroll", () => { + if (isAddingThreads || el.threadList.scrollTop + el.threadList.clientHeight < el.threadList.scrollHeight - 200) return; + const c = el.threadList.children.length; + if (c >= sortedThreads.length) return; + isAddingThreads = true; + const b = sortedThreads.slice(c, c + THREAD_PAGE_SIZE); + if (b.length) { + el.threadList.insertAdjacentHTML("beforeend", b.map(threadRow).join("")); + icons(); + } + isAddingThreads = false; +}); +$(el.threadPopover).on("click", async (e) => { + const act = e.target.closest("[data-action]")?.getAttribute("data-action"); + if (!act || !menuThreadId) return; + const th = THREAD.get(menuThreadId); + if (!th) return; + if (act === "pin") { + th.pinned = !th.pinned; + } else if (act === "rename") { + const nv = prompt("Rename to:", th.title); + if (nv != null) { + th.title = titleFrom(nv); + th.updatedAt = Date.now(); + } + } else if (act === "delete") { + if (confirm("Delete this chat?")) { + THREAD.list = THREAD.list.filter((x) => x.id !== th.id); + if (state.currentThreadId === th.id) { + state.currentThreadId = null; + clearChat(); + } + } + } else if (act === "count_tokens") { + const msgs = Array.isArray(th.messages) ? th.messages : []; + let totalChars = 0; + for (const m of msgs) { + if (!m || !m.role || m.role === "system") continue; + totalChars += String(partsToText(m.content || "") || "").length; + } + const tokens = Math.max(0, Math.ceil(totalChars / 4)); + const k = tokens >= 1e3 ? Math.round(tokens / 1e3) + "k" : String(tokens); + alert(tokens + " tokens (" + k + ")"); + } + hideThreadPopover(); + await THREAD.save(); + renderThreads(); +}); +$(el.suneList).on("click", async (e) => { + const menuBtn = e.target.closest("[data-sune-menu]"); + if (menuBtn) { + e.stopPropagation(); + showSunePopover(menuBtn, menuBtn.getAttribute("[data-sune-menu]") ? menuBtn.getAttribute("[data-sune-menu]") : menuBtn.getAttribute("data-sune-menu")); + return; + } + const btn = e.target.closest("[data-sune-id]"); + if (!btn) return; + const id = btn.getAttribute("data-sune-id"); + if (id) { + if (state.busy) { + state.controller?.disconnect?.(); + setBtnSend(); + state.busy = false; + state.controller = null; + } + SUNE.setActive(id); + renderSidebar(); + await reflectActiveSune(); + state.currentThreadId = null; + clearChat(); + document.getElementById("sidebarLeft").classList.add("-translate-x-full"); + document.getElementById("sidebarOverlayLeft").classList.add("hidden"); + } +}); +$(el.sunePopover).on("click", async (e) => { + const act = e.target.closest("[data-action]")?.getAttribute("data-action"); + if (!act || !menuSuneId) return; + const s = SUNE.get(menuSuneId); + if (!s) return; + const updateAndRender = async () => { + s.updatedAt = Date.now(); + SUNE.save(); + renderSidebar(); + await reflectActiveSune(); + }; + if (act === "pin") { + s.pinned = !s.pinned; + await updateAndRender(); + } else if (act === "rename") { + const n = prompt("Rename sune to:", s.name); + if (n != null) { + s.name = n.trim(); + await updateAndRender(); + } + } else if (act === "pfp") { + const i = document.createElement("input"); + i.type = "file"; + i.accept = "image/*"; + i.onchange = async () => { + const f = i.files?.[0]; + if (!f) return; + try { + s.avatar = await imgToWebp(f); + await updateAndRender(); + } catch { + } + }; + i.click(); + } else if (act === "export") dl(`sune-${(s.name || "sune").replace(/\W/g, "_")}-${ts()}.sune`, [s]); + hideSunePopover(); +}); +function updateAttachBadge() { + const n = state.attachments.length; + el.attachBadge.textContent = String(n); + el.attachBadge.classList.toggle("hidden", n === 0); +} +async function toAttach(file) { + if (!file) return null; + if (file instanceof File) { + const name = file.name || "file", mime = (file.type || "application/octet-stream").toLowerCase(); + if (/^image\//.test(mime) || /\.(png|jpe?g|webp|gif)$/i.test(name)) { + const data2 = mime === "image/webp" || /\.webp$/i.test(name) ? await asDataURL(file) : await imgToWebp(file, 2048, 94); + return { type: "image_url", image_url: { url: data2 } }; + } + if (mime === "application/pdf" || /\.pdf$/i.test(name)) { + const data2 = await asDataURL(file), bin2 = b64(data2); + return { type: "file", file: { filename: name.endsWith(".pdf") ? name : name + ".pdf", file_data: bin2 } }; + } + if (/^audio\//.test(mime) || /\.(wav|mp3)$/i.test(name)) { + const data2 = await asDataURL(file), bin2 = b64(data2), fmt = /mp3/.test(mime) || /\.mp3$/i.test(name) ? "mp3" : "wav"; + return { type: "input_audio", input_audio: { data: bin2, format: fmt } }; + } + const data = await asDataURL(file), bin = b64(data); + return { type: "file", file: { filename: name, file_data: bin } }; + } + if (file && file.name == null && file.data) { + const name = file.name || "file", mime = (file.mime || "application/octet-stream").toLowerCase(); + if (/^image\//.test(mime)) { + const url = `data:${mime};base64,${file.data}`; + return { type: "image_url", image_url: { url } }; + } + if (mime === "application/pdf") { + return { type: "file", file: { filename: name, file_data: file.data } }; + } + if (/^audio\//.test(mime)) { + const fmt = /mp3/.test(mime) ? "mp3" : "wav"; + return { type: "input_audio", input_audio: { data: file.data, format: fmt } }; + } + return { type: "file", file: { filename: name, file_data: file.data } }; + } + return null; +} +$(el.attachBtn).on("click", () => { + if (state.busy) return; + if (state.attachments.length) { + state.attachments = []; + updateAttachBadge(); + el.fileInput.value = ""; + } + el.fileInput.click(); +}); +$(el.fileInput).on("change", async () => { + const files = [...el.fileInput.files || []]; + if (!files.length) return; + for (const f of files) { + const at = await toAttach(f).catch(() => null); + if (at) state.attachments.push(at); + } + updateAttachBadge(); +}); +$(el.composer).on("submit", async (e) => { + e.preventDefault(); + if (state.busy) return; + const text = el.input.value.trim(); + if (!text && !state.attachments.length) return SUNE.infer(); + await ensureThreadOnFirstUser(); + const th = THREAD.active, shouldGenTitle = th && !th.title; + el.input.value = ""; + const parts = []; + if (text) parts.push({ type: "text", text }); + parts.push(...state.attachments); + const userMsg = { role: "user", content: parts.length ? parts : [{ type: "text", text: text || "(sent attachments)" }] }; + addMessage(userMsg); + el.composer.dispatchEvent(new CustomEvent("user:send", { detail: { message: userMsg } })); + if (shouldGenTitle) (async () => { + const title = await generateTitleWithAI(state.messages) || partsToText(state.messages.find((m) => m.role === "user")?.content) || "Untitled"; + await THREAD.setTitle(th.id, title); + })(); + if (!SUNE.model) return state.attachments = [], updateAttachBadge(); + state.busy = true; + setBtnStop(); + const a = SUNE.active, suneMeta = { sune_name: a.name, model: SUNE.model, avatar: a.avatar || "" }, streamId = sid(), suneBubble = addSuneBubbleStreaming(suneMeta, streamId); + suneBubble.dataset.mid = streamId; + const assistantMsg = Object.assign({ id: streamId, role: "assistant", content: [{ type: "text", text: "" }] }, suneMeta); + state.messages.push(assistantMsg); + THREAD.persist(false); + state.stream = { rid: streamId, bubble: suneBubble, meta: suneMeta, text: "", done: false }; + let buf = "", completed = false; + const onDelta = (delta, done) => { + buf += delta; + state.stream.text = buf; + renderMarkdown(suneBubble, buf, { enhance: false }); + assistantMsg.content[0].text = buf; + if (done && !completed) { + completed = true; + setBtnSend(); + state.busy = false; + enhanceCodeBlocks(suneBubble, true); + THREAD.persist(true); + el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: assistantMsg } })); + state.stream = { rid: null, bubble: null, meta: null, text: "", done: false }; + } else if (!done) THREAD.persist(false); + }; + await askOpenRouterStreaming(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_include_thoughts.checked = !!s.include_thoughts; + el.set_ignore_master_prompt.checked = !!s.ignore_master_prompt; + showTab("Model"); + el.suneModal.classList.remove("hidden"); +} +const closeSettings = () => { + el.suneModal.classList.add("hidden"); +}; +const tabs = { Model: ["tabModel", "panelModel"], Prompt: ["tabPrompt", "panelPrompt"], Script: ["tabScript", "panelScript"] }; +function showTab(key) { + Object.entries(tabs).forEach(([k, [tb, pn]]) => { + el[tb].classList.toggle("border-black", k === key); + el[pn].classList.toggle("hidden", k !== key); + }); + if (key === "Prompt") { + ensureJars().then(({ jsonSchema }) => { + const s = SUNE.settings; + jsonSchema.updateCode(s.json_schema || ""); + }); + } else if (key === "Script") { + openedHTML = true; + showHtmlTab("index"); + ensureJars().then(({ html, extension }) => { + const s = SUNE.settings; + html.updateCode(s.html || ""); + extension.updateCode(s.extension_html || ""); + }); + } +} +$(el.suneBtnTop).on("click", openSettings); +$(el.cancelSettings).on("click", closeSettings); +$(el.suneModal).on("click", (e) => { + if (e.target === el.suneModal || e.target.classList.contains("bg-black/30")) closeSettings(); +}); +$(el.tabModel).on("click", () => showTab("Model")); +$(el.tabPrompt).on("click", () => showTab("Prompt")); +$(el.tabScript).on("click", () => showTab("Script")); +$(el.settingsForm).on("submit", async (e) => { + e.preventDefault(); + SUNE.url = (el.suneURL.value || "").trim(); + SUNE.model = (el.set_model.value || "").trim(); + ["temperature", "top_p", "top_k", "frequency_penalty", "repetition_penalty", "min_p", "top_a"].forEach((k) => SUNE[k] = el[`set_${k}`].value.trim()); + SUNE.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.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 window.ensureThreadOnFirstUser(t); - window.addMessage({ role: "user", content: [{ type: "text", text: t }] }); + 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 window.ensureThreadOnFirstUser(clean[0]); + await ensureThreadOnFirstUser(clean[0]); const newMsgs = clean.map((t) => ({ id: gid(), role: "user", content: [{ type: "text", text: t }] })); - window.state.messages.push(...newMsgs); + state.messages.push(...newMsgs); const frag = document.createDocumentFragment(); const newEls = newMsgs.map((m) => { - const $row = window._createMessageRow(m), bubble = $row.find(".msg-bubble")[0]; + 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)); - window.el.messages.appendChild(frag); + el.messages.appendChild(frag); queueMicrotask(() => { newEls.forEach((item) => { - window.renderMarkdown(item.bubbleEl, window.partsToText(item.message.content)); + renderMarkdown(item.bubbleEl, partsToText(item.message.content)); }); - window.el.chat.scrollTo({ top: window.el.chat.scrollHeight, behavior: "smooth" }); - window.icons(); + el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: "smooth" }); + icons(); }); await THREAD.persist(); }, get PAT() { @@ -369,64 +1141,47 @@ const USER = { log: async (s) => { }, set gcpSA(v) { localStorage.setItem("gcp_sa_json", v ? JSON.stringify(v) : ""); } }; -const state = { messages: [], busy: false, controller: null, currentThreadId: null, abortRequested: false, attachments: [], stream: { rid: null, bubble: null, meta: null, text: "", done: false } }; -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; +async function init() { + await SUNE.fetchDotSune("sune-org/store@main/marketplace.sune"); + await THREAD.load(); + await renderThreads(); + renderSidebar(); + await reflectActiveSune(); + clearChat(); + icons(); + kbBind(); + kbUpdate(); } -const HTTP_BASE = "https://orp.aww.4ev.link/ws"; -const cacheStore = localforage.createInstance({ name: "threads_cache", storeName: "streams_status" }); -const localDemoReply = () => "Tip: open the sidebar → Account & Backup to set your API key."; -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; +$(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); }); - return o; -}; +} +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 HTTP_BASE = "https://orp.aww.4ev.link/ws"; const buildBody = () => { const msgs = []; - if (window.USER.masterPrompt && !window.SUNE.ignore_master_prompt) msgs.push({ role: "system", content: [{ type: "text", text: window.USER.masterPrompt }] }); - if (window.SUNE.system_prompt) msgs.push({ role: "system", content: [{ type: "text", text: window.SUNE.system_prompt }] }); - msgs.push(...window.state.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }))); - const b = payloadWithSampling({ model: window.SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/, ""), messages: msgs, stream: true }); - if (window.SUNE.json_output) { + 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(window.SUNE.json_schema || "null"); + s = JSON.parse(SUNE.json_schema || "null"); } catch { s = null; } @@ -436,23 +1191,22 @@ const buildBody = () => { b.response_format = { type: "json_object" }; } } - if (window.SUNE.img_output) b.modalities = ["image", "text"]; - b.reasoning = { ...window.SUNE.reasoning_effort && window.SUNE.reasoning_effort !== "default" ? { effort: window.SUNE.reasoning_effort } : {}, exclude: !window.SUNE.include_thoughts }; - if (window.SUNE.verbosity) b.verbosity = window.SUNE.verbosity; + 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; }; async function askOpenRouterStreaming(onDelta, streamId) { - const model = window.SUNE.model, provider = model.startsWith("oai:") ? "openai" : model.startsWith("g:") ? "google" : model.startsWith("cla:") ? "claude" : model.startsWith("cf:") ? "cloudflare" : model.startsWith("or:") ? "openrouter" : window.USER.provider, apiKey = provider === "openai" ? window.USER.apiKeyOpenAI : provider === "google" ? window.USER.apiKeyGoogle : provider === "claude" ? window.USER.apiKeyClaude : provider === "cloudflare" ? window.USER.apiKeyCloudflare : window.USER.apiKeyOpenRouter; + 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({ text: localDemoReply() }, true); + onDelta(localDemoReply(), true); return { ok: true, rid: streamId || null }; } - const r = { rid: streamId || sid(), seq: -1, done: false, signaled: false, ws: 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); + onDelta(t || "", true); } }; const ws = new WebSocket(HTTP_BASE.replace("https", "wss") + "?uid=" + encodeURIComponent(r.rid)); @@ -467,11 +1221,11 @@ async function askOpenRouterStreaming(onDelta, streamId) { } if (m.type === "delta" && typeof m.seq === "number" && m.seq > r.seq) { r.seq = m.seq; - onDelta(m, false); + onDelta(m.text || "", false); } else if (m.type === "done" || m.type === "err") { r.done = true; cacheStore.setItem(r.rid, "done"); - signal(m); + signal(m.type === "err" ? "\n\n" + (m.message || "error") : ""); ws.close(); } }; @@ -479,7 +1233,7 @@ async function askOpenRouterStreaming(onDelta, streamId) { }; ws.onerror = () => { }; - window.state.controller = { abort: () => { + state.controller = { abort: () => { r.done = true; cacheStore.setItem(r.rid, "done"); try { @@ -490,481 +1244,6 @@ async function askOpenRouterStreaming(onDelta, streamId) { }, disconnect: () => ws.close() }; return { ok: true, rid: r.rid }; } -async function generateTitleWithAI(messages) { - const model = window.USER.titleModel, apiKey = window.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 getBubbleById = (id) => window.el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`); -async function syncActiveThread() { - const id = window.THREAD.getLastAssistantMessageId(); - if (!id) return false; - if (await cacheStore.getItem(id) === "done") { - if (window.state.busy) { - window.setBtnSend(); - window.state.busy = false; - window.state.controller = null; - } - return false; - } - if (!window.state.busy) { - window.state.busy = true; - window.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(); - }; - } }; - window.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) => { - window.renderMarkdown(bubble, t, { enhance: false }); - window.enhanceCodeBlocks(bubble, true); - const i = window.state.messages.findIndex((x) => x.id === id); - if (i >= 0) window.state.messages[i].content = c; - else window.state.messages.push({ id, role: "assistant", content: c, ...window.activeMeta() }); - window.THREAD.persist(); - window.setBtnSend(); - window.state.busy = false; - cacheStore.setItem(id, "done"); - window.state.controller = null; - window.el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: window.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 || "", images = j.images || [], isDone = j.error || j.done || j.phase === "done"; - const content = []; - if (text) content.push({ type: "text", text }); - content.push(...images); - const displayText = window.partsToText(content); - window.renderMarkdown(bubble, displayText, { enhance: false }); - if (isDone) { - finalise(displayText, content); - 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 api = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - askOpenRouterStreaming, - generateTitleWithAI, - localDemoReply, - payloadWithSampling, - syncActiveThread, - syncWhileBusy -}, 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); - }); -}; -const 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_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 md = window.markdownit({ html: false, linkify: true, typographer: true, breaks: true }); -const getModelShort = (m) => { - const mm = m || SUNE.model || ""; - return mm.includes("/") ? mm.split("/").pop() : mm; -}; -const getSuneLabel = (m) => { - const name = m && m.sune_name || SUNE.name, modelShort = getModelShort(m && m.model); - return `${name} · ${modelShort}`; -}; -const partsToText = (parts) => { - if (!parts) return ""; - if (Array.isArray(parts)) return parts.map((p) => p?.type === "text" ? p.text : p?.type === "image_url" ? `![](${p.image_url?.url || ""})` : p?.type === "file" ? `[${p.file?.filename || "file"}]` : p?.type === "input_audio" ? `(audio:${p.input_audio?.format || ""})` : "").join("\n"); - return String(parts); -}; -const activeMeta = () => ({ sune_name: SUNE.name, model: SUNE.model, avatar: SUNE.avatar }); -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 renderMarkdown = (node, text, opt = { enhance: true, highlight: true }) => { - node.innerHTML = md.render(text); - if (opt.enhance) enhanceCodeBlocks(node, opt.highlight); -}; -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.content)); - $(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 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.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 = ""; -}; -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; -} -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 = () => { - const list = [...SUNE.list].sort((a, b) => b.pinned - a.pinned); - el.suneList.innerHTML = list.map(suneRow).join(""); - icons(); -}; -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(); -} -const updateAttachBadge = () => { - const n = state.attachments.length; - el.attachBadge.textContent = String(n); - el.attachBadge.classList.toggle("hidden", n === 0); -}; -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(); -}; -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); - }); -} -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_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 || ""); - }); - } -} -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); - }); -} const accountTabs = { General: ["accountTabGeneral", "accountPanelGeneral"], API: ["accountTabAPI", "accountPanelAPI"], User: ["accountTabUser", "accountPanelUser"] }; function showAccountTab(key) { Object.entries(accountTabs).forEach(([k, [tb, pn]]) => { @@ -992,543 +1271,194 @@ function openAccountSettings() { showAccountTab("General"); el.accountSettingsModal.classList.remove("hidden"); } -const closeAccountSettings = () => { +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."); + } }; -const getActiveHtmlParts = () => !el.htmlEditor.classList.contains("hidden") ? [el.htmlEditor, jars.html] : [el.extensionHtmlEditor, jars.extension]; -function setupEventListeners() { - $(el.threadList).on("click", async (e) => { - const openBtn = e.target.closest("[data-open-thread]"), menuBtn = e.target.closest("[data-thread-menu]"); - if (openBtn) { - const id = openBtn.getAttribute("data-open-thread"); - if (id !== state.currentThreadId && state.busy) { - state.controller?.disconnect?.(); - setBtnSend(); - state.busy = false; - state.controller = null; - } - const th = THREAD.get(id); - if (!th) return; - if (id === state.currentThreadId) { - el.sidebarRight.classList.add("translate-x-full"); - el.sidebarOverlayRight.classList.add("hidden"); - hideThreadPopover(); - return; - } - state.currentThreadId = id; - clearChat(); - state.messages = Array.isArray(th.messages) ? [...th.messages] : []; - for (const m of state.messages) { - const b = msgRow(m); - b.dataset.mid = m.id || ""; - renderMarkdown(b, partsToText(m.content)); - } - await renderSuneHTML(); - syncWhileBusy(); - queueMicrotask(() => el.chat.scrollTo({ top: el.chat.scrollHeight, behavior: "smooth" })); - el.sidebarRight.classList.add("translate-x-full"); - el.sidebarOverlayRight.classList.add("hidden"); - hideThreadPopover(); - return; - } - if (menuBtn) { - e.stopPropagation(); - showThreadPopover(menuBtn, menuBtn.getAttribute("[data-thread-menu]") ? menuBtn.getAttribute("[data-thread-menu]") : menuBtn.getAttribute("data-thread-menu")); - } - }); - $(el.threadList).on("scroll", () => { - if (isAddingThreads || el.threadList.scrollTop + el.threadList.clientHeight < el.threadList.scrollHeight - 200) return; - const c = el.threadList.children.length; - if (c >= sortedThreads.length) return; - isAddingThreads = true; - const b = sortedThreads.slice(c, c + THREAD_PAGE_SIZE); - if (b.length) { - el.threadList.insertAdjacentHTML("beforeend", b.map(threadRow).join("")); - icons(); - } - isAddingThreads = false; - }); - $(el.threadPopover).on("click", async (e) => { - const act = e.target.closest("[data-action]")?.getAttribute("data-action"); - if (!act || !menuThreadId) return; - const th = THREAD.get(menuThreadId); - if (!th) return; - if (act === "pin") { - th.pinned = !th.pinned; - } else if (act === "rename") { - const nv = prompt("Rename to:", th.title); - if (nv != null) { - th.title = titleFrom(nv); - th.updatedAt = Date.now(); - } - } else if (act === "delete") { - if (confirm("Delete this chat?")) { - THREAD.list = THREAD.list.filter((x) => x.id !== th.id); - if (state.currentThreadId === th.id) { - state.currentThreadId = null; - clearChat(); - } - } - } else if (act === "count_tokens") { - const msgs = Array.isArray(th.messages) ? th.messages : []; - let totalChars = 0; - for (const m of msgs) { - if (!m || !m.role || m.role === "system") continue; - totalChars += String(partsToText(m.content || "") || "").length; - } - const tokens = Math.max(0, Math.ceil(totalChars / 4)); - const k = tokens >= 1e3 ? Math.round(tokens / 1e3) + "k" : String(tokens); - alert(tokens + " tokens (" + k + ")"); - } - hideThreadPopover(); - await THREAD.save(); - renderThreads(); - }); - $(el.suneList).on("click", async (e) => { - const menuBtn = e.target.closest("[data-sune-menu]"); - if (menuBtn) { - e.stopPropagation(); - showSunePopover(menuBtn, menuBtn.getAttribute("[data-sune-menu]") ? menuBtn.getAttribute("[data-sune-menu]") : menuBtn.getAttribute("data-sune-menu")); - return; - } - const btn = e.target.closest("[data-sune-id]"); - if (!btn) return; - const id = btn.getAttribute("data-sune-id"); - if (id) { - if (state.busy) { - state.controller?.disconnect?.(); - setBtnSend(); - state.busy = false; - state.controller = null; - } - SUNE.setActive(id); - renderSidebar(); - await reflectActiveSune(); - state.currentThreadId = null; - clearChat(); - document.getElementById("sidebarLeft").classList.add("-translate-x-full"); - document.getElementById("sidebarOverlayLeft").classList.add("hidden"); - } - }); - $(el.sunePopover).on("click", async (e) => { - const act = e.target.closest("[data-action]")?.getAttribute("data-action"); - if (!act || !menuSuneId) return; - const s = SUNE.get(menuSuneId); - if (!s) return; - const updateAndRender = async () => { - s.updatedAt = Date.now(); - SUNE.save(); - renderSidebar(); - await reflectActiveSune(); - }; - if (act === "pin") { - s.pinned = !s.pinned; - await updateAndRender(); - } else if (act === "rename") { - const n = prompt("Rename sune to:", s.name); - if (n != null) { - s.name = n.trim(); - await updateAndRender(); - } - } else if (act === "pfp") { - const i = document.createElement("input"); - i.type = "file"; - i.accept = "image/*"; - i.onchange = async () => { - const f = i.files?.[0]; - if (!f) return; - try { - s.avatar = await imgToWebp(f); - await updateAndRender(); - } catch { - } - }; - i.click(); - } else if (act === "export") dl(`sune-${(s.name || "sune").replace(/\W/g, "_")}-${ts()}.sune`, [s]); - hideSunePopover(); - }); - $(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 window.toAttach(f).catch(() => null); - if (at) state.attachments.push(at); - } - updateAttachBadge(); - }); - $(el.composer).on("submit", async (e) => { - e.preventDefault(); - if (state.busy) return; - const text = el.input.value.trim(); - if (!text && !state.attachments.length) return SUNE.infer(); - await ensureThreadOnFirstUser(); - const th = THREAD.active, shouldGenTitle = th && !th.title; - el.input.value = ""; - const parts = []; - if (text) parts.push({ type: "text", text }); - parts.push(...state.attachments); - const userMsg = { role: "user", content: parts.length ? parts : [{ type: "text", text: text || "(sent attachments)" }] }; - addMessage(userMsg); - el.composer.dispatchEvent(new CustomEvent("user:send", { detail: { message: userMsg } })); - if (shouldGenTitle) (async () => { - const title = await generateTitleWithAI(state.messages) || partsToText(state.messages.find((m) => m.role === "user")?.content) || "Untitled"; - await THREAD.setTitle(th.id, title); - })(); - if (!SUNE.model) return state.attachments = [], updateAttachBadge(); - state.busy = true; - setBtnStop(); - const a = SUNE.active, suneMeta = { sune_name: a.name, model: SUNE.model, avatar: a.avatar || "" }, streamId = window.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 = ""; - const onDelta = (delta, done) => { - if (done) { - if (delta.type === "err" && delta.message) buf += "\n\n" + delta.message; - else if (delta.text) buf += delta.text; - const textPart = assistantMsg.content.find((p) => p.type === "text"); - if (textPart) textPart.text = buf; - else if (buf) assistantMsg.content.unshift({ type: "text", text: buf }); - 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 (delta.text) buf += delta.text; - if (delta.images) assistantMsg.content.push(...delta.images); - const textPart = assistantMsg.content.find((p) => p.type === "text"); - if (textPart) textPart.text = buf; - renderMarkdown(suneBubble, partsToText(assistantMsg.content), { enhance: false }); - THREAD.persist(false); - } - }; - await window.askOpenRouterStreaming(onDelta, streamId); - state.attachments = []; - updateAttachBadge(); - }); - $(el.suneBtnTop).on("click", openSettings); - $(el.cancelSettings).on("click", closeSettings); - $(el.suneModal).on("click", (e) => { - if (e.target === el.suneModal || e.target.classList.contains("bg-black/30")) closeSettings(); - }); - $(el.tabModel).on("click", () => showTab("Model")); - $(el.tabPrompt).on("click", () => showTab("Prompt")); - $(el.tabScript).on("click", () => showTab("Script")); - $(el.settingsForm).on("submit", async (e) => { - e.preventDefault(); - SUNE.url = (el.suneURL.value || "").trim(); - SUNE.model = (el.set_model.value || "").trim(); - ["temperature", "top_p", "top_k", "frequency_penalty", "repetition_penalty", "min_p", "top_a"].forEach((k) => SUNE[k] = el[`set_${k}`].value.trim()); - SUNE.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.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"); - }); - 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) => window.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(SUNE.list.map((s) => [s.id, s])); - Object.values(map).forEach((s) => { - const ex = idx[s.id]; - if (!ex) { - SUNE.list.push(s); - added++; - } else if (+s.updatedAt > +ex.updatedAt) { - Object.assign(ex, s); - updated++; - } - }); - SUNE.save(); - if (data.activeId && SUNE.list.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; - } - }); - $(el.accountSettingsOption).on("click", () => { - el.userMenu.classList.add("hidden"); +$(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(); - }); - $(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."); + alert("Imported."); + } catch { + alert("Import failed"); + } +}; +const getBubbleById = (id) => el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`); +async function syncActiveThread() { + const id = THREAD.getLastAssistantMessageId(); + if (!id) return false; + if (await cacheStore.getItem(id) === "done") { + if (state.busy) { + setBtnSend(); + state.busy = false; + state.controller = null; } + return false; + } + if (!state.busy) { + state.busy = true; + state.controller = { abort: () => { + const ws = new WebSocket(HTTP_BASE.replace("https", "wss")); + ws.onopen = function() { + this.send(JSON.stringify({ type: "stop", rid: id })); + this.close(); + }; + } }; + setBtnStop(); + } + const bubble = getBubbleById(id); + if (!bubble) return false; + const prevText = bubble.textContent || ""; + const j = await fetch(HTTP_BASE + "?uid=" + encodeURIComponent(id)).then((r) => r.ok ? r.json() : null).catch(() => null); + const finalise = (t, c) => { + renderMarkdown(bubble, t, { enhance: false }); + enhanceCodeBlocks(bubble, true); + const i = state.messages.findIndex((x) => x.id === id); + if (i >= 0) state.messages[i].content = c; + else state.messages.push({ id, role: "assistant", content: c, ...activeMeta() }); + THREAD.persist(); + setBtnSend(); + state.busy = false; + cacheStore.setItem(id, "done"); + state.controller = null; + el.composer.dispatchEvent(new CustomEvent("sune:newSuneResponse", { detail: { message: state.messages.find((m) => m.id === id) } })); }; - $(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"); - window.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."); + if (!j || j.rid !== id) { + if (j && j.error) { + const t = prevText + "\n\n" + j.error; + finalise(t, [{ type: "text", text: t }]); } - }; - 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"); - } - }; - $(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 { - } - }); - $(el.copyHTML).on("click", async () => { - try { - await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent || ""); - } catch { - } - }); - $(el.pasteHTML).on("click", async () => { - try { - const t = await navigator.clipboard.readText(); - const [editor, jar] = getActiveHtmlParts(); - if (jar && jar.updateCode) jar.updateCode(t); - else if (editor) editor.textContent = t; - } catch { - } - }); - el.htmlTab_index.textContent = "index.html"; - el.htmlTab_extension.textContent = "extension.html"; - el.htmlTab_index.onclick = () => showHtmlTab("index"); - el.htmlTab_extension.onclick = () => showHtmlTab("extension"); - $(window).on("resize", () => { - hideThreadPopover(); - hideSunePopover(); - }); - const onForeground = () => { - if (document.visibilityState !== "visible") return; - state.controller?.disconnect?.(); - if (state.busy) syncWhileBusy(); - }; - $(document).on("visibilitychange", onForeground); - kbBind(); + 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 ui = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - _createMessageRow, - activeMeta, - addMessage, - addSuneBubbleStreaming, - clearChat, - el, - enhanceCodeBlocks, - ensureThreadOnFirstUser, - getModelShort, - getSuneLabel, - hideSunePopover, - hideThreadPopover, - msgRow, - partsToText, - reflectActiveSune, - renderMarkdown, - renderSidebar, - renderThreads, - setBtnSend, - setBtnStop, - setupEventListeners, - updateAttachBadge -}, Symbol.toStringTag, { value: "Module" })); -(() => { - 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())); - }; -})(); -Object.assign(window, utils, { SUNE, THREAD, USER, state, el, ...api, ...ui }); -async function init() { - await SUNE.fetchDotSune("sune-org/store@main/marketplace.sune"); - await THREAD.load(); - await renderThreads(); - renderSidebar(); - await reflectActiveSune(); - clearChat(); - icons(); - setupEventListeners(); +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; + } } -init(); +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, buildBody, askOpenRouterStreaming, showAccountTab, openAccountSettings, closeAccountSettings, getBubbleById, syncActiveThread, syncWhileBusy, onForeground, getActiveHtmlParts, imgToWebp }); diff --git a/dist/index.html b/dist/index.html index 5780507..ad85459 100644 --- a/dist/index.html +++ b/dist/index.html @@ -11,7 +11,7 @@ - + @@ -92,7 +92,6 @@
-
@@ -159,3 +158,5 @@ + + diff --git a/dist/sw.js b/dist/sw.js index f2ff75a..77dd1eb 100644 --- a/dist/sw.js +++ b/dist/sw.js @@ -1 +1 @@ -if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const c=e=>i(e,t),d={module:{uri:t},exports:o,require:c};s[t]=Promise.all(n.map(e=>d[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-5ffe50d4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-CZ8Js0gk.css",revision:null},{url:"assets/index-pr-7k4fx.js",revision:null},{url:"index.html",revision:"6c20a14a848d346f4ccbb7fa1481dcae"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))}); +if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const d=e=>i(e,t),l={module:{uri:t},exports:o,require:d};s[t]=Promise.all(n.map(e=>l[e]||d(e))).then(e=>(r(...e),o))}}define(["./workbox-5ffe50d4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"assets/index-CU6EhWVp.js",revision:null},{url:"assets/index-CZ8Js0gk.css",revision:null},{url:"index.html",revision:"bd9259dd07e73a8a42a241e9107ac1fc"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});