Compare commits

28 Commits

Author SHA1 Message Date
github-actions[bot]
4fcda90f81 This build was committed by a bot. 2026-05-16 05:24:24 +00:00
0a3d8093cb Feat: Show chat scrollbar only on desktop devices 2026-05-15 22:24:12 -07:00
github-actions[bot]
7b650e0c3b This build was committed by a bot. 2026-05-16 05:11:55 +00:00
cbfb79257b Fix: Reset busy UI when sync fetch fails 2026-05-15 22:11:40 -07:00
github-actions[bot]
6db81f99fe This build was committed by a bot. 2026-04-19 03:45:59 +00:00
67dc535244 Update default model version to 4.7 2026-04-18 20:45:45 -07:00
53ba91caaf Docs: Trim SECURITY.md further 2026-04-18 16:18:46 -07:00
github-actions[bot]
6f8afc3cb7 This build was committed by a bot. 2026-04-10 03:02:58 +00:00
160f1a5315 Fix: Use innerText so highlighter preserves newlines
Co-authored-by: Opus 4.6 <noreply@anthropic.com>
2026-04-09 20:02:43 -07:00
github-actions[bot]
d98041a053 This build was committed by a bot. 2026-04-10 02:48:49 +00:00
6da497f8d3 Update highlight.js to version 11.11.1 2026-04-09 19:48:35 -07:00
github-actions[bot]
7841842b7f This build was committed by a bot. 2026-04-10 02:45:39 +00:00
7e6a271872 Update index.html 2026-04-09 19:45:24 -07:00
8f87fd8259 Fix: Prevent newline vanishing in code editors 2026-04-09 19:39:03 -07:00
github-actions[bot]
c60cf5930b This build was committed by a bot. 2026-04-10 02:36:04 +00:00
44a4905463 Fix: Use CodeJar toString() to preserve newlines in editors
Co-authored-by: Opus 4.6 <noreply@anthropic.com>
2026-04-09 19:35:50 -07:00
github-actions[bot]
6b0209adec This build was committed by a bot. 2026-04-10 02:14:31 +00:00
89455a31c9 Fix: CodeJar newline deletion and saving bugs 2026-04-09 19:14:17 -07:00
github-actions[bot]
f41130793d This build was committed by a bot. 2026-04-09 21:38:16 +00:00
e190890af0 Feat: Hook up customKey1 load/save and import/export functionality 2026-04-09 14:38:00 -07:00
38f763a986 Feat: Add Custom Key 1 field to API tab 2026-04-09 14:37:54 -07:00
c554646bb0 Feat: Register set_api_key_custom1 DOM node 2026-04-09 14:37:47 -07:00
f90deaa15f Feat: Add customKey1 to USER object 2026-04-09 14:37:43 -07:00
4a1f6836dc Update Sunes image in README 2026-04-02 17:20:19 -07:00
github-actions[bot]
5741d15a35 This build was committed by a bot. 2026-04-03 00:19:26 +00:00
0532a6f030 Add files via upload 2026-04-02 17:19:04 -07:00
github-actions[bot]
4b933bf5e4 This build was committed by a bot. 2026-04-03 00:07:24 +00:00
7b20d3940b Fix: Render text accurately on single-shot completions 2026-04-02 17:07:11 -07:00
15 changed files with 91 additions and 53 deletions

11
SECURITY.md Normal file
View File

@@ -0,0 +1,11 @@
# Security & Privacy
Sune is client-side. Your threads, sunes, settings, API keys, and GitHub PAT live in your browser's `localStorage` / `IndexedDB` — never on our servers. GitHub sync (if enabled) goes browser → your repo with your PAT. No accounts, no tracking.
## The Proxy
Streaming is relayed through [`us.proxy.sune.chat`](https://github.com/sune-org/us.proxy.sune.chat) so mobile generations survive screen locks. Your browser opens a WebSocket, the proxy forwards to the provider with the API key **you** supplied, and streams tokens back.
**What it doesn't do:** no prompt logging (messages sit in `:memory:` SQLite with a 20-min TTL for reconnects, never written to disk), no reading your chats, no storing keys, no third-party sharing.
**What I see:** an [ntfy](https://ntfy.sh) ping when a run ends/fails, containing only: run ID, `[provider/model]`, duration, and error message (if any). No prompts, no responses, no IP, no key. Source is public — audit it [here](https://github.com/sune-org/us.proxy.sune.chat).

BIN
dist/appstore_content/sune_sidebar.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

View File

@@ -300,6 +300,7 @@ var el = window.el = Object.fromEntries([
"set_api_key_g", "set_api_key_g",
"set_api_key_claude", "set_api_key_claude",
"set_api_key_cf", "set_api_key_cf",
"set_api_key_custom1",
"set_title_model", "set_title_model",
"copySystemPrompt", "copySystemPrompt",
"pasteSystemPrompt", "pasteSystemPrompt",
@@ -490,6 +491,12 @@ var USER = {
}, },
set githubToken(v) { set githubToken(v) {
localStorage.setItem("gh_token", v || ""); localStorage.setItem("gh_token", v || "");
},
get customKey1() {
return localStorage.getItem("custom_key_1") || "";
},
set customKey1(v) {
localStorage.setItem("custom_key_1", v || "");
} }
}; };
//#endregion //#endregion
@@ -800,7 +807,7 @@ var __vitePreload = function preload(baseModule, deps, importerUrl) {
o != k && ((k = o) ? (history.pushState({ k: 1 }, ""), addEventListener("popstate", f)) : (removeEventListener("popstate", f), history.state?.k && history.back())); o != k && ((k = o) ? (history.pushState({ k: 1 }, ""), addEventListener("popstate", f)) : (removeEventListener("popstate", f), history.state?.k && history.back()));
}; };
})(); })();
var DEFAULT_MODEL = "anthropic/claude-opus-4.6"; var DEFAULT_MODEL = "anthropic/claude-opus-4.7";
var icons = () => window.lucide && lucide.createIcons(); var icons = () => window.lucide && lucide.createIcons();
var haptic = () => /android/i.test(navigator.userAgent) && navigator.vibrate?.(1); var haptic = () => /android/i.test(navigator.userAgent) && navigator.vibrate?.(1);
var su = { var su = {
@@ -965,8 +972,8 @@ var SUNE = window.SUNE = new Proxy({
} }
buf += delta; buf += delta;
state.stream.text = buf; state.stream.text = buf;
renderMarkdown(suneBubble, partsToText(assistantMsg), { enhance: false });
assistantMsg.content[0].text = buf; assistantMsg.content[0].text = buf;
renderMarkdown(suneBubble, partsToText(assistantMsg), { enhance: false });
if (done && !completed) { if (done && !completed) {
completed = true; completed = true;
setBtnSend(); setBtnSend();
@@ -1589,8 +1596,8 @@ $(el.composer).on("submit", async (e) => {
} }
buf += delta; buf += delta;
state.stream.text = buf; state.stream.text = buf;
renderMarkdown(suneBubble, partsToText(assistantMsg), { enhance: false });
assistantMsg.content[0].text = buf; assistantMsg.content[0].text = buf;
renderMarkdown(suneBubble, partsToText(assistantMsg), { enhance: false });
if (done && !completed) { if (done && !completed) {
completed = true; completed = true;
setBtnSend(); setBtnSend();
@@ -1617,7 +1624,11 @@ var jars = {
}; };
var ensureJars = async () => { var ensureJars = async () => {
if (jars.html && jars.extension) return jars; if (jars.html && jars.extension) return jars;
const mod = await __vitePreload(() => import("https://medv.io/codejar/codejar.js"), []), CodeJar = mod.CodeJar || mod.default, hl = (e) => e.innerHTML = hljs.highlight(e.textContent, { language: "xml" }).value; const mod = await __vitePreload(() => import("https://medv.io/codejar/codejar.js"), []), CodeJar = mod.CodeJar || mod.default;
const hl = (e) => {
const code = e.innerText;
e.innerHTML = hljs.highlight(code, { language: "xml" }).value;
};
if (!jars.html) jars.html = CodeJar(el.htmlEditor, hl, { tab: " " }); if (!jars.html) jars.html = CodeJar(el.htmlEditor, hl, { tab: " " });
if (!jars.extension) jars.extension = CodeJar(el.extensionHtmlEditor, hl, { tab: " " }); if (!jars.extension) jars.extension = CodeJar(el.extensionHtmlEditor, hl, { tab: " " });
return jars; return jars;
@@ -1703,8 +1714,8 @@ $(el.settingsForm).on("submit", async (e) => {
SUNE.include_thoughts = el.set_include_thoughts.checked; SUNE.include_thoughts = el.set_include_thoughts.checked;
SUNE.ignore_master_prompt = el.set_ignore_master_prompt.checked; SUNE.ignore_master_prompt = el.set_ignore_master_prompt.checked;
if (openedHTML) { if (openedHTML) {
SUNE.html = el.htmlEditor.textContent; SUNE.html = jars.html.toString();
SUNE.extension_html = el.extensionHtmlEditor.textContent; SUNE.extension_html = jars.extension.toString();
} }
closeSettings(); closeSettings();
await reflectActiveSune(); await reflectActiveSune();
@@ -2063,6 +2074,7 @@ function openAccountSettings() {
el.set_api_key_g.value = USER.apiKeyGoogle || ""; el.set_api_key_g.value = USER.apiKeyGoogle || "";
el.set_api_key_claude.value = USER.apiKeyClaude || ""; el.set_api_key_claude.value = USER.apiKeyClaude || "";
el.set_api_key_cf.value = USER.apiKeyCloudflare || ""; el.set_api_key_cf.value = USER.apiKeyCloudflare || "";
el.set_api_key_custom1.value = USER.customKey1 || "";
el.set_master_prompt.value = USER.masterPrompt || ""; el.set_master_prompt.value = USER.masterPrompt || "";
el.set_title_model.value = USER.titleModel; el.set_title_model.value = USER.titleModel;
el.set_gh_token.value = USER.githubToken || ""; el.set_gh_token.value = USER.githubToken || "";
@@ -2092,6 +2104,7 @@ $(el.accountSettingsForm).on("submit", (e) => {
USER.apiKeyGoogle = String(el.set_api_key_g.value || "").trim(); USER.apiKeyGoogle = String(el.set_api_key_g.value || "").trim();
USER.apiKeyClaude = String(el.set_api_key_claude.value || "").trim(); USER.apiKeyClaude = String(el.set_api_key_claude.value || "").trim();
USER.apiKeyCloudflare = String(el.set_api_key_cf.value || "").trim(); USER.apiKeyCloudflare = String(el.set_api_key_cf.value || "").trim();
USER.customKey1 = String(el.set_api_key_custom1.value || "").trim();
USER.masterPrompt = String(el.set_master_prompt.value || "").trim(); USER.masterPrompt = String(el.set_master_prompt.value || "").trim();
USER.titleModel = String(el.set_title_model.value || "").trim(); USER.titleModel = String(el.set_title_model.value || "").trim();
USER.githubToken = String(el.set_gh_token.value || "").trim(); USER.githubToken = String(el.set_gh_token.value || "").trim();
@@ -2111,19 +2124,6 @@ $(el.accountPanelAPI).on("click", (e) => {
el.accountTabGeneral.onclick = () => showAccountTab("General"); el.accountTabGeneral.onclick = () => showAccountTab("General");
el.accountTabAPI.onclick = () => showAccountTab("API"); el.accountTabAPI.onclick = () => showAccountTab("API");
el.accountTabUser.onclick = () => showAccountTab("User"); 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`, { el.exportAccountSettings.onclick = () => dl(`sune-account-${ts()}.json`, {
v: 1, v: 1,
provider: USER.provider, provider: USER.provider,
@@ -2132,6 +2132,7 @@ el.exportAccountSettings.onclick = () => dl(`sune-account-${ts()}.json`, {
apiKeyGoogle: USER.apiKeyGoogle, apiKeyGoogle: USER.apiKeyGoogle,
apiKeyClaude: USER.apiKeyClaude, apiKeyClaude: USER.apiKeyClaude,
apiKeyCloudflare: USER.apiKeyCloudflare, apiKeyCloudflare: USER.apiKeyCloudflare,
customKey1: USER.customKey1,
masterPrompt: USER.masterPrompt, masterPrompt: USER.masterPrompt,
titleModel: USER.titleModel, titleModel: USER.titleModel,
githubToken: USER.githubToken, githubToken: USER.githubToken,
@@ -2155,6 +2156,7 @@ el.importAccountSettingsInput.onchange = async (e) => {
apiKeyGoogle: "apiKeyG", apiKeyGoogle: "apiKeyG",
apiKeyClaude: "apiKeyC", apiKeyClaude: "apiKeyC",
apiKeyCloudflare: "apiKeyCF", apiKeyCloudflare: "apiKeyCF",
customKey1: "customKey1",
masterPrompt: "masterPrompt", masterPrompt: "masterPrompt",
titleModel: "titleModel", titleModel: "titleModel",
githubToken: "ghToken", githubToken: "ghToken",
@@ -2197,7 +2199,14 @@ async function syncActiveThread() {
setBtnStop(); setBtnStop();
} }
const bubble = getBubbleById(id); const bubble = getBubbleById(id);
if (!bubble) return false; if (!bubble) {
if (state.busy) {
setBtnSend();
state.busy = false;
state.controller = null;
}
return false;
}
const msgIdx = state.messages.findIndex((x) => x.id === id); const msgIdx = state.messages.findIndex((x) => x.id === id);
const localText = msgIdx >= 0 ? partsToText(state.messages[msgIdx]) : bubble.textContent || ""; const localText = msgIdx >= 0 ? partsToText(state.messages[msgIdx]) : bubble.textContent || "";
const j = await fetch(HTTP_BASE + "?uid=" + encodeURIComponent(id)).then((r) => r.ok ? r.json() : null).catch(() => null); const j = await fetch(HTTP_BASE + "?uid=" + encodeURIComponent(id)).then((r) => r.ok ? r.json() : null).catch(() => null);
@@ -2231,6 +2240,13 @@ async function syncActiveThread() {
type: "text", type: "text",
text: t text: t
}]); }]);
} else {
await cacheStore.setItem(id, "done");
if (state.busy) {
setBtnSend();
state.busy = false;
state.controller = null;
}
} }
return false; return false;
} }
@@ -2288,18 +2304,18 @@ $(el.pasteSystemPrompt).on("click", async () => {
el.set_system_prompt.value = await navigator.clipboard.readText(); el.set_system_prompt.value = await navigator.clipboard.readText();
} catch {} } catch {}
}); });
var getActiveHtmlParts = () => !el.htmlEditor.classList.contains("hidden") ? [el.htmlEditor, jars.html] : [el.extensionHtmlEditor, jars.extension]; var getActiveJar = () => !el.htmlEditor.classList.contains("hidden") ? jars.html : jars.extension;
$(el.copyHTML).on("click", async () => { $(el.copyHTML).on("click", async () => {
try { try {
await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent || ""); const jar = getActiveJar();
await navigator.clipboard.writeText(jar ? jar.toString() : "");
} catch {} } catch {}
}); });
$(el.pasteHTML).on("click", async () => { $(el.pasteHTML).on("click", async () => {
try { try {
const t = await navigator.clipboard.readText(); const t = await navigator.clipboard.readText();
const [editor, jar] = getActiveHtmlParts(); const jar = getActiveJar();
if (jar && jar.updateCode) jar.updateCode(t); if (jar) jar.updateCode(t);
else if (editor) editor.textContent = t;
} catch {} } catch {}
}); });
Object.assign(window, { Object.assign(window, {
@@ -2364,7 +2380,7 @@ Object.assign(window, {
syncActiveThread, syncActiveThread,
syncWhileBusy, syncWhileBusy,
onForeground, onForeground,
getActiveHtmlParts, getActiveJar,
imgToWebp, imgToWebp,
cacheStore, cacheStore,
ghApi, ghApi,

View File

@@ -2,8 +2,7 @@
:root{--safe-bottom:env(safe-area-inset-bottom)} :root{--safe-bottom:env(safe-area-inset-bottom)}
::-webkit-scrollbar{height:8px;width:8px} ::-webkit-scrollbar{height:8px;width:8px}
::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px} ::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}
.no-scrollbar::-webkit-scrollbar{display:none} @media(pointer: coarse){.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}}
.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}
html,body{overscroll-behavior-y:contain;font-family:'Assistant',sans-serif} html,body{overscroll-behavior-y:contain;font-family:'Assistant',sans-serif}
.markdown-body{font-size:14px;line-height:1.6} .markdown-body{font-size:14px;line-height:1.6}
.markdown-body pre{overflow:auto} .markdown-body pre{overflow:auto}

6
dist/index.html vendored
View File

@@ -13,8 +13,8 @@
<script defer src="//unpkg.com/alpinejs"></script> <script defer src="//unpkg.com/alpinejs"></script>
<script type="module" crossorigin src="/assets/index-B9Cmf6JU.js"></script> <script type="module" crossorigin src="/assets/index-DVG7SzRn.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CLEI5Rwr.css"> <link rel="stylesheet" crossorigin href="/assets/index-DaGRC7Kr.css">
<link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head> <link rel="manifest" href="/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script></head>
<body class="bg-white text-gray-900 selection:bg-black/10" x-data @click.window="if($event.target.closest('button')) haptic(); if(!document.getElementById('threadPopover').contains($event.target)&&!$event.target.closest('[data-thread-menu]')) hideThreadPopover(); if(!document.getElementById('sunePopover').contains($event.target)&&!$event.target.closest('[data-sune-menu]')) hideSunePopover(); if(!document.getElementById('userMenu').contains($event.target)&&!document.getElementById('userMenuBtn').contains($event.target)) document.getElementById('userMenu').classList.add('hidden')"> <body class="bg-white text-gray-900 selection:bg-black/10" x-data @click.window="if($event.target.closest('button')) haptic(); if(!document.getElementById('threadPopover').contains($event.target)&&!$event.target.closest('[data-thread-menu]')) hideThreadPopover(); if(!document.getElementById('sunePopover').contains($event.target)&&!$event.target.closest('[data-sune-menu]')) hideSunePopover(); if(!document.getElementById('userMenu').contains($event.target)&&!document.getElementById('userMenuBtn').contains($event.target)) document.getElementById('userMenu').classList.add('hidden')">
<div class="flex flex-col h-dvh max-h-dvh overflow-hidden"> <div class="flex flex-col h-dvh max-h-dvh overflow-hidden">
@@ -169,7 +169,7 @@
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
</div> </div>
<div id="accountPanelAPI" class="p-4 hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div></div></div> <div id="accountPanelAPI" class="p-4 hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Custom Key 1</label><div class="relative"><input id="set_api_key_custom1" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_custom1" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.customKey1</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div></div></div>
<div id="accountPanelUser" class="p-4 space-y-4 hidden"> <div id="accountPanelUser" class="p-4 space-y-4 hidden">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div> <div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div>

2
dist/sw.js vendored
View File

@@ -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} didnt 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),l={module:{uri:t},exports:o,require:c};s[t]=Promise.all(n.map(e=>l[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-8c29f6e4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"index.html",revision:"3f53914e4f9b60ba0800f4bdcf84cf66"},{url:"assets/index-CLEI5Rwr.css",revision:null},{url:"assets/index-B9Cmf6JU.js",revision:null},{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} didnt 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-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"index.html",revision:"af2178b9db3800fa6deda68db3fe0707"},{url:"assets/index-DaGRC7Kr.css",revision:null},{url:"assets/index-DVG7SzRn.js",revision:null},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});

File diff suppressed because one or more lines are too long

1
dist/workbox-9c191d2f.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

View File

@@ -4,7 +4,7 @@
> Each sune is like a module. You can have many. And share them. > Each sune is like a module. You can have many. And share them.
![Sunes](./public/appstore_content/screenshot6.jpg) ![Sunes](./public/appstore_content/sune_sidebar.png)
![Setting](./public/appstore_content/screenshot3.jpg) ![Setting](./public/appstore_content/screenshot3.jpg)

View File

@@ -14,7 +14,7 @@ export const el = window.el = Object.fromEntries(
'htmlTab_index','htmlTab_extension','suneHtml','accountSettingsModal','accountSettingsForm', 'htmlTab_index','htmlTab_extension','suneHtml','accountSettingsModal','accountSettingsForm',
'closeAccountSettings','cancelAccountSettings','set_master_prompt','set_provider', '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_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', 'set_api_key_custom1','set_title_model','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML',
'accountTabGeneral','accountTabAPI','accountPanelGeneral','accountPanelAPI','set_gh_token', 'accountTabGeneral','accountTabAPI','accountPanelGeneral','accountPanelAPI','set_gh_token',
'importAccountSettings','exportAccountSettings', 'importAccountSettings','exportAccountSettings',
'importAccountSettingsInput','accountTabUser','accountPanelUser','set_user_name', 'importAccountSettingsInput','accountTabUser','accountPanelUser','set_user_name',

View File

@@ -13,7 +13,7 @@ import { titleFrom, serializeThreadName, deserializeThreadName } from './threads
import { resolveSuneSrc, processSuneIncludes, renderSuneHTML } from './sune-html.js'; import { resolveSuneSrc, processSuneIncludes, renderSuneHTML } from './sune-html.js';
(()=>{let k,v=visualViewport;const f=()=>{removeEventListener('popstate',f),document.activeElement?.blur()};v.onresize=()=>{let o=v.height<innerHeight;o!=k&&((k=o)?(history.pushState({k:1},''),addEventListener('popstate',f)):(removeEventListener('popstate',f),history.state?.k&&history.back()))}})() (()=>{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='anthropic/claude-opus-4.6' const DEFAULT_MODEL='anthropic/claude-opus-4.7'
const icons=()=>window.lucide&&lucide.createIcons() const icons=()=>window.lucide&&lucide.createIcons()
const haptic=()=>/android/i.test(navigator.userAgent)&&navigator.vibrate?.(1) const haptic=()=>/android/i.test(navigator.userAgent)&&navigator.vibrate?.(1)
@@ -21,7 +21,7 @@ const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse
const defaultSettings={model:DEFAULT_MODEL,temperature:'',top_p:'',top_k:'',frequency_penalty:'',repetition_penalty:'',min_p:'',top_a:'',verbosity:'',reasoning_effort:'default',system_prompt:'',html:'',extension_html:"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",hide_composer:false,include_thoughts:false,img_output:false,aspect_ratio:'1:1',image_size:'1K',ignore_master_prompt:false} const defaultSettings={model:DEFAULT_MODEL,temperature:'',top_p:'',top_k:'',frequency_penalty:'',repetition_penalty:'',min_p:'',top_a:'',verbosity:'',reasoning_effort:'default',system_prompt:'',html:'',extension_html:"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",hide_composer:false,include_thoughts:false,img_output:false,aspect_ratio:'1:1',image_size:'1K',ignore_master_prompt:false}
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 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) let sunes=(su.load()||[]).map(makeSune)
const SUNE=window.SUNE=new Proxy({get list(){return sunes},get id(){return su.getActiveId()},get active(){return sunes.find(a=>a.id===su.getActiveId())||sunes[0]},get:id=>sunes.find(s=>s.id===id),setActive:id=>su.setActiveId(id||''),create(p={}){const s=makeSune(p);sunes.unshift(s);su.save(sunes);return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);if(sunes.length===0){const def=this.create({name:'Default'});this.setActive(def.id)}else if(curId===id)this.setActive(sunes[0].id)},save:()=>su.save(sunes)},{get(t,p){if(p==='fetchDotSune')return async g=>{try{const u=g.startsWith('http')?g:(()=>{const[a,b]=g.split('@'),[c,d]=a.split('/'),[e,...f]=b.split('/');return`https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join('/')}`})(),j=await(await fetch(u)).json(),l=sunes.length;sunes.unshift(...(Array.isArray(j)?j:j?.sunes||[]).filter(s=>s?.id&&!t.get(s.id)).map(s=>makeSune(s)));sunes.length>l&&t.save()}catch{}};if(p==='attach')return async files=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser('(attachments)');addMessage({role:'assistant',content:clean,...activeMeta()});await THREAD.persist()};if(p==='log')return async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await THREAD.persist()};if(p==='lastReply')return [...state.messages].reverse().find(m=>m.role==='assistant');if(p==='infer')return async()=>{if(state.busy||!SUNE.model||state.abortRequested){state.abortRequested=false;return};await ensureThreadOnFirstUser('Sune Inference');const th=THREAD.active;if(th&&!th.title)(async()=>THREAD.setTitle(th.id,await generateTitleWithAI(state.messages)||'Sune Inference'))();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:null,bubble:null,meta:null,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId)};if(p==='getByName')return n=>sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(p==='handoff')return async n=>{await new Promise(r=>setTimeout(r,4000));const s=sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(!s)return;SUNE.setActive(s.id);renderSidebar();await reflectActiveSune();await SUNE.infer()};if(p in t)return t[p];const a=t.active;if(!a)return;if(p in a.settings)return a.settings[p];if(p in a)return a[p]},set(t,p,v){const a=t.active;if(!a)return false;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return false;const isTopLevel=/^(name|avatar|url|pinned|storage)$/.test(p),target=isTopLevel?sunes[i]:sunes[i].settings;let value=v;if(!isTopLevel){if(p==='system_prompt')value=v||''}if(target[p]!==value){target[p]=value;sunes[i].updatedAt=Date.now();su.save(sunes)}return true}}) const SUNE=window.SUNE=new Proxy({get list(){return sunes},get id(){return su.getActiveId()},get active(){return sunes.find(a=>a.id===su.getActiveId())||sunes[0]},get:id=>sunes.find(s=>s.id===id),setActive:id=>su.setActiveId(id||''),create(p={}){const s=makeSune(p);sunes.unshift(s);su.save(sunes);return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);if(sunes.length===0){const def=this.create({name:'Default'});this.setActive(def.id)}else if(curId===id)this.setActive(sunes[0].id)},save:()=>su.save(sunes)},{get(t,p){if(p==='fetchDotSune')return async g=>{try{const u=g.startsWith('http')?g:(()=>{const[a,b]=g.split('@'),[c,d]=a.split('/'),[e,...f]=b.split('/');return`https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join('/')}`})(),j=await(await fetch(u)).json(),l=sunes.length;sunes.unshift(...(Array.isArray(j)?j:j?.sunes||[]).filter(s=>s?.id&&!t.get(s.id)).map(s=>makeSune(s)));sunes.length>l&&t.save()}catch{}};if(p==='attach')return async files=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser('(attachments)');addMessage({role:'assistant',content:clean,...activeMeta()});await THREAD.persist()};if(p==='log')return async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await THREAD.persist()};if(p==='lastReply')return [...state.messages].reverse().find(m=>m.role==='assistant');if(p==='infer')return async()=>{if(state.busy||!SUNE.model||state.abortRequested){state.abortRequested=false;return};await ensureThreadOnFirstUser('Sune Inference');const th=THREAD.active;if(th&&!th.title)(async()=>THREAD.setTitle(th.id,await generateTitleWithAI(state.messages)||'Sune Inference'))();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:null,bubble:null,meta:null,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;assistantMsg.content[0].text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId)};if(p==='getByName')return n=>sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(p==='handoff')return async n=>{await new Promise(r=>setTimeout(r,4000));const s=sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(!s)return;SUNE.setActive(s.id);renderSidebar();await reflectActiveSune();await SUNE.infer()};if(p in t)return t[p];const a=t.active;if(!a)return;if(p in a.settings)return a.settings[p];if(p in a)return a[p]},set(t,p,v){const a=t.active;if(!a)return false;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return false;const isTopLevel=/^(name|avatar|url|pinned|storage)$/.test(p),target=isTopLevel?sunes[i]:sunes[i].settings;let value=v;if(!isTopLevel){if(p==='system_prompt')value=v||''}if(target[p]!==value){target[p]=value;sunes[i].updatedAt=Date.now();su.save(sunes)}return true}})
if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)} if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)}
const state=window.state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:false}} const 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 getModelShort=m=>{const mm=m||SUNE.model||'';return mm.includes('/')?mm.split('/').pop():mm}
@@ -77,8 +77,19 @@ $(el.sunePopover).on('click',async e=>{const act=e.target.closest('[data-action]
function updateAttachBadge(){const n=state.attachments.length;el.attachBadge.textContent=String(n);el.attachBadge.classList.toggle('hidden',n===0)} function updateAttachBadge(){const n=state.attachments.length;el.attachBadge.textContent=String(n);el.attachBadge.classList.toggle('hidden',n===0)}
$(el.attachBtn).on('click',()=>{if(state.busy)return;if(state.attachments.length){state.attachments=[];updateAttachBadge();el.fileInput.value=''};el.fileInput.click()}) $(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.fileInput).on('change',async()=>{const files=[...(el.fileInput.files||[])];if(!files.length)return;for(const f of files){const at=await toAttach(f).catch(()=>null);if(at)state.attachments.push(at)}updateAttachBadge()})
$(el.composer).on('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return SUNE.infer();await ensureThreadOnFirstUser(text||'(attachments)');const th=THREAD.active,shouldGenTitle=th&&!th.title;el.input.value='';const parts=[];if(text)parts.push({type:'text',text});parts.push(...state.attachments);const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};addMessage(userMsg);el.composer.dispatchEvent(new CustomEvent('user:send',{detail:{message:userMsg}}));if(shouldGenTitle)(async()=>{const title=await generateTitleWithAI(state.messages)||partsToText(state.messages.find(m=>m.role==='user')).replace(/!\[\]\(data:[^\)]+\)/g,'[Image]')||'Untitled';await THREAD.setTitle(th.id,title)})();if(!SUNE.model)return state.attachments=[],updateAttachBadge();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});assistantMsg.content[0].text=buf;if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId);state.attachments=[];updateAttachBadge()}) $(el.composer).on('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return SUNE.infer();await ensureThreadOnFirstUser(text||'(attachments)');const th=THREAD.active,shouldGenTitle=th&&!th.title;el.input.value='';const parts=[];if(text)parts.push({type:'text',text});parts.push(...state.attachments);const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};addMessage(userMsg);el.composer.dispatchEvent(new CustomEvent('user:send',{detail:{message:userMsg}}));if(shouldGenTitle)(async()=>{const title=await generateTitleWithAI(state.messages)||partsToText(state.messages.find(m=>m.role==='user')).replace(/!\[\]\(data:[^\)]+\)/g,'[Image]')||'Untitled';await THREAD.setTitle(th.id,title)})();if(!SUNE.model)return state.attachments=[],updateAttachBadge();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;assistantMsg.content[0].text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId);state.attachments=[];updateAttachBadge()})
let jars={html:null,extension:null};const ensureJars=async()=>{if(jars.html&&jars.extension)return jars;const mod=await import('https://medv.io/codejar/codejar.js'),CodeJar=mod.CodeJar||mod.default,hl=e=>e.innerHTML=hljs.highlight(e.textContent,{language:'xml'}).value;if(!jars.html)jars.html=CodeJar(el.htmlEditor,hl,{tab:' '});if(!jars.extension)jars.extension=CodeJar(el.extensionHtmlEditor,hl,{tab:' '});return jars} let jars={html:null,extension:null};
const ensureJars=async()=>{
if(jars.html&&jars.extension)return jars;
const mod=await import('https://medv.io/codejar/codejar.js'),CodeJar=mod.CodeJar||mod.default;
const hl=e=>{
const code=e.innerText;
e.innerHTML=hljs.highlight(code,{language:'xml'}).value;
};
if(!jars.html)jars.html=CodeJar(el.htmlEditor,hl,{tab:' '});
if(!jars.extension)jars.extension=CodeJar(el.extensionHtmlEditor,hl,{tab:' '});
return jars;
}
let openedHTML=false 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_img_output.checked=!!s.img_output;el.set_aspect_ratio.value=s.aspect_ratio||'1:1';el.set_image_size.value=s.image_size||'1K';el.aspectRatioContainer.classList.toggle('hidden',!s.img_output);el.set_include_thoughts.checked=!!s.include_thoughts;el.set_ignore_master_prompt.checked=!!s.ignore_master_prompt;showTab('Model');el.suneModal.classList.remove('hidden')} 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_img_output.checked=!!s.img_output;el.set_aspect_ratio.value=s.aspect_ratio||'1:1';el.set_image_size.value=s.image_size||'1K';el.aspectRatioContainer.classList.toggle('hidden',!s.img_output);el.set_include_thoughts.checked=!!s.include_thoughts;el.set_ignore_master_prompt.checked=!!s.ignore_master_prompt;showTab('Model');el.suneModal.classList.remove('hidden')}
const closeSettings=()=>{el.suneModal.classList.add('hidden')} const closeSettings=()=>{el.suneModal.classList.add('hidden')}
@@ -91,7 +102,7 @@ $(el.tabModel).on('click',()=>showTab('Model'))
$(el.tabPrompt).on('click',()=>showTab('Prompt')) $(el.tabPrompt).on('click',()=>showTab('Prompt'))
$(el.tabScript).on('click',()=>showTab('Script')) $(el.tabScript).on('click',()=>showTab('Script'))
$(el.set_img_output).on('change',e=>el.aspectRatioContainer.classList.toggle('hidden',!e.target.checked)) $(el.set_img_output).on('change',e=>el.aspectRatioContainer.classList.toggle('hidden',!e.target.checked))
$(el.settingsForm).on('submit',async e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a'].forEach(k=>SUNE[k]=el[`set_${k}`].value.trim());SUNE.verbosity=(el.set_verbosity.value||'');SUNE.reasoning_effort=(el.set_reasoning_effort.value||'default');SUNE.system_prompt=el.set_system_prompt.value.trim();SUNE.hide_composer=el.set_hide_composer.checked;SUNE.img_output=el.set_img_output.checked;SUNE.aspect_ratio=el.set_aspect_ratio.value;SUNE.image_size=el.set_image_size.value;SUNE.include_thoughts=el.set_include_thoughts.checked;SUNE.ignore_master_prompt=el.set_ignore_master_prompt.checked;if(openedHTML){SUNE.html=el.htmlEditor.textContent;SUNE.extension_html=el.extensionHtmlEditor.textContent}closeSettings();await reflectActiveSune()}) $(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.img_output=el.set_img_output.checked;SUNE.aspect_ratio=el.set_aspect_ratio.value;SUNE.image_size=el.set_image_size.value;SUNE.include_thoughts=el.set_include_thoughts.checked;SUNE.ignore_master_prompt=el.set_ignore_master_prompt.checked;if(openedHTML){SUNE.html=jars.html.toString();SUNE.extension_html=jars.extension.toString()}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.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')}) $(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 let importMode=null
@@ -143,29 +154,28 @@ $(el.threadFolderBtn).on('click',async()=>{const n=prompt('Folder name:');if(!n)
$(el.threadSyncBtn).on('click',async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const mode=confirm('Sync Threads:\nOK = Upload (Push)\nCancel = Download (Pull)');const info=parseGhUrl(u);try{if(mode){const remoteItems=await ghApi(`${info.apiPath}?ref=${info.branch}`)||[],remoteMap={};remoteItems.forEach(i=>{const d=deserializeThreadName(i.name);if(d)remoteMap[d.id]={name:i.name,sha:i.sha}});const toRemove=[];for(const t of THREAD.list){if(t.status==='deleted'){if(remoteMap[t.id]){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Delete thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch});await localforage.removeItem('rem_t_'+t.id)}toRemove.push(t.id);continue}if(t.type!=='thread')continue;if(t.status==='modified'||t.status==='new'){const newName=serializeThreadName(t),msgs=await localforage.getItem('rem_t_'+t.id);if(remoteMap[t.id]&&remoteMap[t.id].name!==newName){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Rename thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch})}const x=await ghApi(`${info.apiPath}/${newName}?ref=${info.branch}`);await ghApi(`${info.apiPath}/${newName}`,'PUT',{message:`Sync thread ${t.id}`,content:utob(JSON.stringify(msgs,null,2)),branch:info.branch,sha:x?.sha});t.status='synced'}}THREAD.list=THREAD.list.filter(x=>!toRemove.includes(x.id));await THREAD.save();alert('Pushed to GitHub.')}else{await pullThreads();alert('Pulled from GitHub.')}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}}); $(el.threadSyncBtn).on('click',async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const mode=confirm('Sync Threads:\nOK = Upload (Push)\nCancel = Download (Pull)');const info=parseGhUrl(u);try{if(mode){const remoteItems=await ghApi(`${info.apiPath}?ref=${info.branch}`)||[],remoteMap={};remoteItems.forEach(i=>{const d=deserializeThreadName(i.name);if(d)remoteMap[d.id]={name:i.name,sha:i.sha}});const toRemove=[];for(const t of THREAD.list){if(t.status==='deleted'){if(remoteMap[t.id]){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Delete thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch});await localforage.removeItem('rem_t_'+t.id)}toRemove.push(t.id);continue}if(t.type!=='thread')continue;if(t.status==='modified'||t.status==='new'){const newName=serializeThreadName(t),msgs=await localforage.getItem('rem_t_'+t.id);if(remoteMap[t.id]&&remoteMap[t.id].name!==newName){await ghApi(`${info.apiPath}/${remoteMap[t.id].name}`,'DELETE',{message:`Rename thread ${t.id}`,sha:remoteMap[t.id].sha,branch:info.branch})}const x=await ghApi(`${info.apiPath}/${newName}?ref=${info.branch}`);await ghApi(`${info.apiPath}/${newName}`,'PUT',{message:`Sync thread ${t.id}`,content:utob(JSON.stringify(msgs,null,2)),branch:info.branch,sha:x?.sha});t.status='synced'}}THREAD.list=THREAD.list.filter(x=>!toRemove.includes(x.id));await THREAD.save();alert('Pushed to GitHub.')}else{await pullThreads();alert('Pulled from GitHub.')}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}});
init() init()
const accountTabs={General:['accountTabGeneral','accountPanelGeneral'],API:['accountTabAPI','accountPanelAPI'],User:['accountTabUser','accountPanelUser']};function showAccountTab(key){Object.entries(accountTabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)})} const accountTabs={General:['accountTabGeneral','accountPanelGeneral'],API:['accountTabAPI','accountPanelAPI'],User:['accountTabUser','accountPanelUser']};function showAccountTab(key){Object.entries(accountTabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)})}
function openAccountSettings(){el.set_provider.value=USER.provider||'openrouter';el.set_api_key_or.value=USER.apiKeyOpenRouter||'';el.set_api_key_oai.value=USER.apiKeyOpenAI||'';el.set_api_key_g.value=USER.apiKeyGoogle||'';el.set_api_key_claude.value=USER.apiKeyClaude||'';el.set_api_key_cf.value=USER.apiKeyCloudflare||'';el.set_master_prompt.value=USER.masterPrompt||'';el.set_title_model.value=USER.titleModel;el.set_gh_token.value=USER.githubToken||'';el.set_user_name.value=USER.name;el.userAvatarPreview.src=USER.avatar||'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';el.userAvatarPreview.classList.toggle('bg-gray-200',!USER.avatar);showAccountTab('General');el.accountSettingsModal.classList.remove('hidden')} function openAccountSettings(){el.set_provider.value=USER.provider||'openrouter';el.set_api_key_or.value=USER.apiKeyOpenRouter||'';el.set_api_key_oai.value=USER.apiKeyOpenAI||'';el.set_api_key_g.value=USER.apiKeyGoogle||'';el.set_api_key_claude.value=USER.apiKeyClaude||'';el.set_api_key_cf.value=USER.apiKeyCloudflare||'';el.set_api_key_custom1.value=USER.customKey1||'';el.set_master_prompt.value=USER.masterPrompt||'';el.set_title_model.value=USER.titleModel;el.set_gh_token.value=USER.githubToken||'';el.set_user_name.value=USER.name;el.userAvatarPreview.src=USER.avatar||'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';el.userAvatarPreview.classList.toggle('bg-gray-200',!USER.avatar);showAccountTab('General');el.accountSettingsModal.classList.remove('hidden')}
function closeAccountSettings(){el.accountSettingsModal.classList.add('hidden')} function closeAccountSettings(){el.accountSettingsModal.classList.add('hidden')}
$(el.accountSettingsOption).on('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()}) $(el.accountSettingsOption).on('click',()=>{el.userMenu.classList.add('hidden');openAccountSettings()})
$(el.closeAccountSettings).on('click',closeAccountSettings) $(el.closeAccountSettings).on('click',closeAccountSettings)
$(el.cancelAccountSettings).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.accountSettingsModal).on('click',e=>{if(e.target===el.accountSettingsModal||e.target.classList.contains('bg-black/30'))closeAccountSettings()})
$(el.accountSettingsForm).on('submit',e=>{e.preventDefault();USER.provider=el.set_provider.value||'openrouter';USER.apiKeyOpenRouter=String(el.set_api_key_or.value||'').trim();USER.apiKeyOpenAI=String(el.set_api_key_oai.value||'').trim();USER.apiKeyGoogle=String(el.set_api_key_g.value||'').trim();USER.apiKeyClaude=String(el.set_api_key_claude.value||'').trim();USER.apiKeyCloudflare=String(el.set_api_key_cf.value||'').trim();USER.masterPrompt=String(el.set_master_prompt.value||'').trim();USER.titleModel=String(el.set_title_model.value||'').trim();USER.githubToken=String(el.set_gh_token.value||'').trim();USER.name=String(el.set_user_name.value||'').trim();closeAccountSettings()}) $(el.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.customKey1=String(el.set_api_key_custom1.value||'').trim();USER.masterPrompt=String(el.set_master_prompt.value||'').trim();USER.titleModel=String(el.set_title_model.value||'').trim();USER.githubToken=String(el.set_gh_token.value||'').trim();USER.name=String(el.set_user_name.value||'').trim();closeAccountSettings()})
$(el.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.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.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,customKey1:USER.customKey1,masterPrompt:USER.masterPrompt,titleModel:USER.titleModel,githubToken:USER.githubToken,userName:USER.name,userAvatar:USER.avatar});
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,userName:USER.name,userAvatar:USER.avatar});
el.importAccountSettings.onclick=()=>{el.importAccountSettingsInput.value='';el.importAccountSettingsInput.click()}; 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'};Object.entries(m).forEach(([p,k])=>{const v=d[p]??d[k];if(typeof v==='string')USER[p]=v});openAccountSettings();alert('Imported.')}catch{alert('Import failed')}}; 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',customKey1:'customKey1',masterPrompt:'masterPrompt',titleModel:'titleModel',githubToken:'ghToken',name:'userName',avatar:'userAvatar'};Object.entries(m).forEach(([p,k])=>{const v=d[p]??d[k];if(typeof v==='string')USER[p]=v});openAccountSettings();alert('Imported.')}catch{alert('Import failed')}};
const getBubbleById=id=>el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`) 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 msgIdx=state.messages.findIndex(x=>x.id===id);const localText=msgIdx>=0?partsToText(state.messages[msgIdx]):(bubble.textContent||'');const j=await(fetch(HTTP_BASE+'?uid='+encodeURIComponent(id)).then(r=>r.ok?r.json():null).catch(()=>null));const finalise=(t,c,imgs)=>{const tempMsg={content:c,images:imgs};renderMarkdown(bubble,partsToText(tempMsg),{enhance:false});enhanceCodeBlocks(bubble,true);if(msgIdx>=0){state.messages[msgIdx].content=c;state.messages[msgIdx].images=imgs}else state.messages.push({id,role:'assistant',content:c,images:imgs,...activeMeta()});THREAD.persist();setBtnSend();state.busy=false;cacheStore.setItem(id,'done');state.controller=null;el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:state.messages.find(m=>m.id===id)}}))};if(!j||j.rid!==id){if(j&&j.error){const t=localText+'\n\n'+j.error;finalise(t,[{type:'text',text:t}])}return false}const serverText=j.text||'',isDone=j.error||j.done||j.phase==='done';const finalText=(serverText.length>=localText.length||isDone)?serverText:localText;const display=partsToText({content:[{type:'text',text:finalText}],images:j.images});if(display)renderMarkdown(bubble,display,{enhance:false});if(isDone){if(finalText!==localText){finalise(finalText,[{type:'text',text:finalText}],j.images)}else{await cacheStore.setItem(id,'done');if(state.busy){setBtnSend();state.busy=false;state.controller=null}}return false}await cacheStore.setItem(id,'busy');return true} 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){if(state.busy){setBtnSend();state.busy=false;state.controller=null;}return false;}const msgIdx=state.messages.findIndex(x=>x.id===id);const localText=msgIdx>=0?partsToText(state.messages[msgIdx]):(bubble.textContent||'');const j=await(fetch(HTTP_BASE+'?uid='+encodeURIComponent(id)).then(r=>r.ok?r.json():null).catch(()=>null));const finalise=(t,c,imgs)=>{const tempMsg={content:c,images:imgs};renderMarkdown(bubble,partsToText(tempMsg),{enhance:false});enhanceCodeBlocks(bubble,true);if(msgIdx>=0){state.messages[msgIdx].content=c;state.messages[msgIdx].images=imgs}else state.messages.push({id,role:'assistant',content:c,images:imgs,...activeMeta()});THREAD.persist();setBtnSend();state.busy=false;cacheStore.setItem(id,'done');state.controller=null;el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:state.messages.find(m=>m.id===id)}}))};if(!j||j.rid!==id){if(j&&j.error){const t=localText+'\n\n'+j.error;finalise(t,[{type:'text',text:t}])}else{await cacheStore.setItem(id,'done');if(state.busy){setBtnSend();state.busy=false;state.controller=null;}}return false}const serverText=j.text||'',isDone=j.error||j.done||j.phase==='done';const finalText=(serverText.length>=localText.length||isDone)?serverText:localText;const display=partsToText({content:[{type:'text',text:finalText}],images:j.images});if(display)renderMarkdown(bubble,display,{enhance:false});if(isDone){if(finalText!==localText){finalise(finalText,[{type:'text',text:finalText}],j.images)}else{await cacheStore.setItem(id,'done');if(state.busy){setBtnSend();state.busy=false;state.controller=null}}return false}await cacheStore.setItem(id,'busy');return true}
let syncLoopRunning=false 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}} async function syncWhileBusy(){if(syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=true;try{while(await syncActiveThread())await new Promise(r=>setTimeout(r,1500))}finally{syncLoopRunning=false}}
const onForeground=()=>{if(document.visibilityState!=='visible')return;state.controller?.disconnect?.();if(state.busy)syncWhileBusy()} const onForeground=()=>{if(document.visibilityState!=='visible')return;state.controller?.disconnect?.();if(state.busy)syncWhileBusy()}
$(document).on('visibilitychange',onForeground) $(document).on('visibilitychange',onForeground)
$(el.copySystemPrompt).on('click',async()=>{try{await navigator.clipboard.writeText(el.set_system_prompt.value||'')}catch{}}) $(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.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] const getActiveJar=()=>!el.htmlEditor.classList.contains('hidden')?jars.html:jars.extension
$(el.copyHTML).on('click',async()=>{try{await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent||'')}catch{}}) $(el.copyHTML).on('click',async()=>{try{const jar=getActiveJar();await navigator.clipboard.writeText(jar?jar.toString():'')}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.pasteHTML).on('click',async()=>{try{const t=await navigator.clipboard.readText();const jar=getActiveJar();if(jar)jar.updateCode(t)}catch{}})
Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,enhanceCodeBlocks,getSuneLabel,_createMessageRow,msgRow,partsToText,addSuneBubbleStreaming,clearChat,payloadWithSampling,setBtnStop,setBtnSend,localDemoReply,titleFrom,serializeThreadName,deserializeThreadName,ensureThreadOnFirstUser,generateTitleWithAI,threadRow,renderThreads,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,updateAttachBadge,toAttach,ensureJars,openSettings,closeSettings,showTab,dl,ts,kbUpdate,kbBind,activeMeta,init,showHtmlTab,showAccountTab,openAccountSettings,closeAccountSettings,getBubbleById,syncActiveThread,syncWhileBusy,onForeground,getActiveHtmlParts,imgToWebp,cacheStore,ghApi,parseGhUrl,pullThreads}); Object.assign(window,{icons,haptic,clamp,num,int,gid,esc,positionPopover,sid,fmtSize,asDataURL,b64,makeSune,getModelShort,resolveSuneSrc,processSuneIncludes,renderSuneHTML,reflectActiveSune,suneRow,enhanceCodeBlocks,getSuneLabel,_createMessageRow,msgRow,partsToText,addSuneBubbleStreaming,clearChat,payloadWithSampling,setBtnStop,setBtnSend,localDemoReply,titleFrom,serializeThreadName,deserializeThreadName,ensureThreadOnFirstUser,generateTitleWithAI,threadRow,renderThreads,hideThreadPopover,showThreadPopover,hideSunePopover,showSunePopover,updateAttachBadge,toAttach,ensureJars,openSettings,closeSettings,showTab,dl,ts,kbUpdate,kbBind,activeMeta,init,showHtmlTab,showAccountTab,openAccountSettings,closeAccountSettings,getBubbleById,syncActiveThread,syncWhileBusy,onForeground,getActiveJar,imgToWebp,cacheStore,ghApi,parseGhUrl,pullThreads});

View File

@@ -95,7 +95,7 @@
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
</div> </div>
<div id="accountPanelAPI" class="p-4 hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div></div></div> <div id="accountPanelAPI" class="p-4 hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Custom Key 1</label><div class="relative"><input id="set_api_key_custom1" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_custom1" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.customKey1</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div></div></div>
<div id="accountPanelUser" class="p-4 space-y-4 hidden"> <div id="accountPanelUser" class="p-4 space-y-4 hidden">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div> <div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div>

View File

@@ -1,5 +1,5 @@
@import url(https://fonts.bunny.net/css?family=assistant:500); @import url(https://fonts.bunny.net/css?family=assistant:500);
:root{--safe-bottom:env(safe-area-inset-bottom)}::-webkit-scrollbar{height:8px;width:8px}::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none} :root{--safe-bottom:env(safe-area-inset-bottom)}::-webkit-scrollbar{height:8px;width:8px}::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}@media(pointer: coarse){.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}}
html,body{overscroll-behavior-y:contain;font-family:'Assistant',sans-serif} html,body{overscroll-behavior-y:contain;font-family:'Assistant',sans-serif}
.markdown-body{font-size:14px;line-height:1.6}.markdown-body pre{overflow:auto} .markdown-body{font-size:14px;line-height:1.6}.markdown-body pre{overflow:auto}
.markdown-body ul,.markdown-body ol{list-style:revert;padding-left:2em} .markdown-body ul,.markdown-body ol{list-style:revert;padding-left:2em}

View File

@@ -33,5 +33,7 @@ export const USER = {
get titleModel() { return localStorage.getItem('title_model') ?? 'or:amazon/nova-micro-v1'; }, get titleModel() { return localStorage.getItem('title_model') ?? 'or:amazon/nova-micro-v1'; },
set titleModel(v) { localStorage.setItem('title_model', v || ''); }, set titleModel(v) { localStorage.setItem('title_model', v || ''); },
get githubToken() { return localStorage.getItem('gh_token') || ''; }, get githubToken() { return localStorage.getItem('gh_token') || ''; },
set githubToken(v) { localStorage.setItem('gh_token', v || ''); } set githubToken(v) { localStorage.setItem('gh_token', v || ''); },
get customKey1() { return localStorage.getItem('custom_key_1') || ''; },
set customKey1(v) { localStorage.setItem('custom_key_1', v || ''); }
}; };