25 Commits

Author SHA1 Message Date
github-actions[bot]
25b4b982ac This build was committed by a bot. 2026-09-17 03:56:13 +00:00
916f41606c Submit composer with Enter on desktop 2026-09-16 20:55:57 -07:00
a1fd2d5a05 Add license agreement for software usage and reuse
Added a comprehensive license agreement outlining usage, ownership, and reuse conditions for the software.
2026-09-14 21:15:24 -07:00
9ce77bd05e chore: update GitHub Sponsors funding 2026-09-14 18:20:24 -07:00
github-actions[bot]
4cf6926453 This build was committed by a bot. 2026-09-11 00:00:17 +00:00
a73ee38f01 Support clipboard file paste in composer and attaching during stream 2026-09-10 16:57:26 -07:00
github-actions[bot]
18aa73e9c8 This build was committed by a bot. 2026-09-10 05:04:58 +00:00
599989fbed Shorten model name placeholder hint 2026-09-09 22:03:58 -07:00
27cbdea778 Support model name placeholder in system prompts 2026-09-09 21:59:49 -07:00
github-actions[bot]
4dd5916862 This build was committed by a bot. 2026-09-08 04:03:35 +00:00
5d27b9904a Style: Adjust topbar padding to py-2
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 21:03:19 -07:00
github-actions[bot]
0bc73c7e80 This build was committed by a bot. 2026-09-08 04:02:49 +00:00
22f578dbd1 Feat: Update topbar avatar image to h-10 w-10
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 21:02:33 -07:00
5c9fe41f1c Feat: Set topbar Sune avatar to h-10 w-10
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 21:02:31 -07:00
github-actions[bot]
c028b505c9 This build was committed by a bot. 2026-09-08 03:52:11 +00:00
567a082fda Perf: Default avatar resize to 96px and 75q
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 20:51:52 -07:00
1a134c82ca Feat: Update topbar avatar image to h-9 w-9
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 20:51:37 -07:00
c362744e39 Feat: Enlarge topbar Sune avatar to h-9 w-9
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 20:51:35 -07:00
github-actions[bot]
3c2fd49227 This build was committed by a bot. 2026-09-08 03:18:00 +00:00
2e840b4b86 refactor(utils): update imgToWebp defaults to D=96 and q=75 2026-09-07 20:17:35 -07:00
github-actions[bot]
4b6ef90d3a This build was committed by a bot. 2026-09-06 04:03:51 +00:00
6c4376ca71 Set omitted reasoning_effort to empty string 2026-09-05 20:59:46 -07:00
github-actions[bot]
dec4bd1fb3 This build was committed by a bot. 2026-09-06 01:23:14 +00:00
a4f04d3aac Replace verbosity setting with max tokens (output) 2026-09-05 18:20:23 -07:00
8b9adc8d64 Update readme.md 2026-09-05 14:34:40 -07:00
13 changed files with 178 additions and 56 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1 +1 @@
custom: https://paypal.me/planetrenox
github: multipleof4

85
LICENSE Normal file
View File

@@ -0,0 +1,85 @@
1. Scope
The copyright holders offering software, source code, assets, or other content
under these terms ("we") grant the permissions below to every individual, team,
company, organization, and other entity ("you"). "Work" means the software,
source code, assets, and other content we offer under these terms.
2. Using the product and owning your output
You may use the customer-facing software or product for any purpose, including
commercial purposes, without payment, acknowledgment, or a copy of these terms.
This includes installing, running, accessing, and self-hosting it, and compiling
it solely to run it.
Whatever you create through ordinary use of the product is yours. We claim no
ownership of your output and impose no payment, acknowledgment, or licensing
conditions on it. You may use, sell, share, or license it however you choose.
Merely copying or extracting the underlying codebase or assetbase is reuse
under section 3.
3. Reusing code or assets
Subject to section 4, you have worldwide, nonexclusive permission to copy,
modify, combine, publish, distribute, sublicense, sell, and otherwise reuse any
or all of the Work, for any purpose, in source or other forms.
4. Finding common ground
If you reuse the codebase or assetbase, you are responsible for satisfying one
of the following conditions when you begin that reuse:
- If you cannot reasonably afford a monetary payment, give a simple
acknowledgment of the original software or repository in an appropriate
place, such as your README, source code, website, or documentation. A name
mention or a link is enough. No particular wording or placement is required.
- If you can afford a monetary payment, sponsor the original
repository through its Sponsor button. You alone decide the minimum
reasonable amount you can afford, considering the circumstances of the entity
reusing the Work. Sponsorship may be one-time or monthly. If you pay any
positive amount, acknowledgment and links are optional.
An entity may satisfy this condition through someone acting on its behalf.
5. Final settlement
Any positive amount successfully paid through that sponsorship route fully
and permanently satisfies the reuse condition for the entity making it or on
whose behalf it is made. Your chosen amount is accepted as sufficient,
regardless of your financial means.
We will not challenge its adequacy, request financial evidence, or demand a
larger amount. A record of the completed payment is sufficient proof; no
separate approval or agreement is needed.
The first completed monthly payment is enough. Later payments are optional,
and you may cancel recurring sponsorship without losing your permissions.
Likewise, an appropriate acknowledgment fully satisfies the condition for an
entity using the acknowledgment option.
Once satisfied, the condition covers all reuse by that entity of the Work
offered under these terms in the same original repository, including later
versions. No payment or acknowledgment is required again for additional uses,
copies, releases, or products. The permissions granted are then irrevocable
for that Work.
6. Choosing another license
After satisfying section 4, you may distribute or sublicense the Work,
modified or unmodified, under any license you choose. You do not have to include
these terms, keep this license, or require anyone else to follow it. These
terms require no retained copyright or license notices; the acknowledgment
option in section 4 is the only acknowledgment requirement.
Recipients of the Work you distribute under another license follow that
license and owe us no payment or acknowledgment under these terms for that
Work. Relicensing grants permissions; it does not transfer ownership of
the original Work.
7. Warranty
To the extent permitted by law, the Work is provided "as is," without any
warranty, including merchantability, fitness for a particular purpose, or
noninfringement. We are not liable for any claim, loss, or damages arising from
the Work or its use.

View File

@@ -40,7 +40,8 @@ var buildBody = () => {
role: "system",
content: mPrompt
});
const sPrompt = (SUNE.system_prompt || "").trim();
const modelName = SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/, "").replace(/^[^/]*\//, "").replace(/(?::online|:free)+$/, "");
const sPrompt = (SUNE.system_prompt || "").trim().replaceAll("{{model_name}}", () => modelName);
if (sPrompt) msgs.push({
role: "system",
content: sPrompt
@@ -70,7 +71,6 @@ var buildBody = () => {
...SUNE.reasoning_effort && SUNE.reasoning_effort !== "default" ? { effort: SUNE.reasoning_effort } : {},
exclude: !SUNE.include_thoughts
};
if (SUNE.verbosity) b.verbosity = SUNE.verbosity;
if (SUNE.img_output) {
b.modalities = ["image"];
b.image_config = {
@@ -250,7 +250,7 @@ var el = window.el = Object.fromEntries([
"set_repetition_penalty",
"set_min_p",
"set_top_a",
"set_verbosity",
"set_max_tokens",
"set_reasoning_effort",
"set_system_prompt",
"set_hide_composer",
@@ -372,7 +372,7 @@ var asDataURL = (f) => new Promise((r) => {
fr.onload = () => r(String(fr.result || ""));
fr.readAsDataURL(f);
});
var imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => {
var imgToWebp = (f, D = 96, q = 75) => new Promise((r, j) => {
if (!f) return j();
const i = new Image();
i.onload = () => {
@@ -646,6 +646,11 @@ function kbUpdate() {
el.chat.style.scrollPaddingBottom = fh + overlap + 16 + "px";
}
function kbBind() {
el.input.addEventListener("keydown", (e) => {
if (e.key !== "Enter" || e.shiftKey || e.isComposing || e.keyCode === 229 || e.repeat || !matchMedia("(hover: hover) and (pointer: fine)").matches) return;
e.preventDefault();
el.composer.requestSubmit();
});
if (window.visualViewport) ["resize", "scroll"].forEach((ev) => window.visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true }));
window.$(window).on("resize orientationchange", () => setTimeout(kbUpdate, 50));
window.$(el.input).on("focus click", () => {
@@ -1026,8 +1031,8 @@ var defaultSettings = {
repetition_penalty: "",
min_p: "",
top_a: "",
verbosity: "",
reasoning_effort: "default",
max_tokens: "",
reasoning_effort: "",
system_prompt: "",
html: "",
extension_html: "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",
@@ -1244,7 +1249,7 @@ var getModelShort = (m) => {
var reflectActiveSune = async () => {
const a = SUNE.active;
el.suneBtnTop.title = `Settings — ${a.name}`;
el.suneBtnTop.innerHTML = a.avatar ? `<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>` : "✺";
el.suneBtnTop.innerHTML = a.avatar ? `<img src="${esc(a.avatar)}" alt="" class="h-10 w-10 rounded-full object-cover"/>` : "✺";
el.footer.classList.toggle("hidden", !!a.settings.hide_composer);
await renderSuneHTML();
icons();
@@ -1341,7 +1346,8 @@ var payloadWithSampling = (b) => {
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)
top_a: num(s.top_a, null),
max_tokens: int(s.max_tokens, null) > 0 ? int(s.max_tokens, null) : null
};
Object.keys(p).forEach((k) => {
const v = p[k];
@@ -1736,7 +1742,6 @@ function updateAttachBadge() {
el.attachBadge.classList.toggle("hidden", n === 0);
}
$(el.attachBtn).on("click", () => {
if (state.busy) return;
if (state.attachments.length) {
state.attachments = [];
updateAttachBadge();
@@ -1753,6 +1758,18 @@ $(el.fileInput).on("change", async () => {
}
updateAttachBadge();
});
$(el.composer).on("paste", async (e) => {
const files = [...(e.clipboardData || e.originalEvent?.clipboardData)?.files || []];
if (!files.length) return;
e.preventDefault();
state.attachments = [];
el.fileInput.value = "";
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;
@@ -1767,6 +1784,9 @@ $(el.composer).on("submit", async (e) => {
text
});
parts.push(...state.attachments);
state.attachments = [];
updateAttachBadge();
el.fileInput.value = "";
const userMsg = {
role: "user",
content: parts.length ? parts : [{
@@ -1780,7 +1800,7 @@ $(el.composer).on("submit", async (e) => {
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();
if (!SUNE.model) return;
state.busy = true;
setBtnStop();
const a = SUNE.active, suneMeta = {
@@ -1834,8 +1854,6 @@ $(el.composer).on("submit", async (e) => {
} else if (!done) THREAD.persist(false);
};
await streamChat(onDelta, streamId);
state.attachments = [];
updateAttachBadge();
});
var jars = {
html: null,
@@ -1865,8 +1883,8 @@ function openSettings() {
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_max_tokens.value = s.max_tokens || "";
el.set_reasoning_effort.value = s.reasoning_effort || "";
el.set_system_prompt.value = s.system_prompt;
el.set_hide_composer.checked = !!s.hide_composer;
el.set_img_output.checked = !!s.img_output;
@@ -1921,10 +1939,10 @@ $(el.settingsForm).on("submit", async (e) => {
"frequency_penalty",
"repetition_penalty",
"min_p",
"top_a"
"top_a",
"max_tokens"
].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.reasoning_effort = el.set_reasoning_effort.value || "";
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;

11
dist/index.html vendored
View File

@@ -13,15 +13,15 @@
<script defer src="//unpkg.com/alpinejs"></script>
<script type="module" crossorigin src="/assets/index-TjHmRKFx.js"></script>
<script type="module" crossorigin src="/assets/index-CWYcSOgO.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DUC1RW1F.css">
<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')">
<div class="flex flex-col h-dvh max-h-dvh overflow-hidden">
<header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
<div class="mx-auto w-full max-w-none px-4 py-2 grid grid-cols-3 items-center">
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
<button id="suneBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings"></button>
<button id="suneBtnTop" class="justify-self-center h-10 w-10 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings"></button>
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
</div>
</header>
@@ -106,7 +106,7 @@
<form id="settingsForm" class="text-sm">
<div class="border-b flex text-xs font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">HTML</button></div>
<div id="panelModel" class="p-4 space-y-4">
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(02)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(01)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
@@ -115,7 +115,7 @@
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(02)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(01)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(01)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Verbosity</label><select id="set_verbosity" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div>
<div><label class="block text-gray-700 font-medium mb-1">Max Tokens (output)</label><input id="set_max_tokens" type="number" min="1" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder=""/></div>
</div>
<div class="flex flex-wrap items-center gap-2 pt-2">
<div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div>
@@ -151,6 +151,7 @@
</div>
<div id="panelPrompt" class="p-4 space-y-4 hidden">
<div><div class="flex items-center justify-between mb-1"><label for="set_system_prompt" class="block text-gray-700 font-medium">System Prompt</label><div class="flex gap-2"><button type="button" id="copySystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteSystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Paste</button></div></div><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Enter a system prompt to guide the sune"></textarea></div>
<p class="text-xs text-gray-500">Use <code>{{model_name}}</code> to insert the model name.</p>
</div>
<div id="panelScript" class="p-1 hidden">
<div class="border-b flex text-xs font-medium"><button type="button" id="htmlTab_index" class="flex-1 py-2 px-3 text-center border-b-2"></button><button type="button" id="htmlTab_extension" class="flex-1 py-2 px-3 text-center border-b-2"></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 l=e=>i(e,t),c={module:{uri:t},exports:o,require:l};s[t]=Promise.all(n.map(e=>c[e]||l(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:"a64ac52329387d11ea67cf7b0a2b461d"},{url:"assets/index-TjHmRKFx.js",revision:null},{url:"assets/index-DUC1RW1F.css",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 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-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"index.html",revision:"f8c767b8fe6e11fa13e704d51b97ffbc"},{url:"assets/index-DUC1RW1F.css",revision:null},{url:"assets/index-CWYcSOgO.js",revision:null},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});

View File

@@ -1,30 +1,31 @@
[![Download For Android (.apk)](https://img.shields.io/badge/Download-For%20Android%20(.apk)-green?style=for-the-badge&logo=android)](https://github.com/multipleof4/sune/releases/download/v0.23.0/sune-v0.23.0.apk)
![Main](./public/appstore_content/screenshot1.jpg)
<p align="center">
<img src="./public/appstore_content/screenshot1.jpg" alt="Main" width="280" />
</p>
> Each sune is like a module. You can have many. And share them.
![Sunes](./public/appstore_content/sune_sidebar.png)
![Setting](./public/appstore_content/screenshot3.jpg)
<p align="center">
<img src="./public/appstore_content/sune_sidebar.png" alt="Sunes" width="280" />
<img src="./public/appstore_content/screenshot3.jpg" alt="Setting" width="280" />
</p>
💠 New!
> You can have scripts which run on the page of each sune — either to function call or extend functionality of the app or sune.
![Scripting](./public/appstore_content/screenshot4.jpg)
<p align="center">
<img src="./public/appstore_content/screenshot4.jpg" alt="Scripting" width="280" />
<img src="./public/appstore_content/screenshot_miku.png" alt="Miku" width="280" />
</p>
> Image support.
> There is a marketplace and LaTeX support out of the box.
![Miku](./public/appstore_content/screenshot_miku.png)
> There is a marketplace.
![Marketplace](./public/appstore_content/screenshot_marketplace.jpg)
> LaTeX support out of the box.
![LaTeX](./public/appstore_content/latex.png)
<p align="center">
<img src="./public/appstore_content/screenshot_marketplace.jpg" alt="Marketplace" width="280" />
<img src="./public/appstore_content/latex.png" alt="LaTeX" width="280" />
</p>
---
@@ -32,7 +33,9 @@
Never lose a conversation again. Sune can sync all your threads to a GitHub repo.
![Sync](./public/appstore_content/sync.png)
<p align="center">
<img src="./public/appstore_content/sync.png" alt="Sync" width="320" />
</p>
### Setup for Syncing Chats

View File

@@ -3,7 +3,7 @@ export 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_frequency_penalty','set_repetition_penalty','set_min_p','set_top_a','set_max_tokens',
'set_reasoning_effort','set_system_prompt','set_hide_composer','set_include_thoughts',
'set_img_output','set_aspect_ratio','set_image_size','aspectRatioContainer',
'set_ignore_master_prompt','deleteSuneBtn','sidebarLeft','sidebarOverlayLeft','sidebarBtnLeft',

View File

@@ -11,6 +11,11 @@ export function kbUpdate() {
}
export function kbBind() {
el.input.addEventListener('keydown', e => {
if (e.key !== 'Enter' || e.shiftKey || e.isComposing || e.keyCode === 229 || e.repeat || !matchMedia('(hover: hover) and (pointer: fine)').matches) return;
e.preventDefault();
el.composer.requestSubmit();
});
if (window.visualViewport) {
['resize', 'scroll'].forEach(ev => window.visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true }));
}

View File

@@ -57,14 +57,14 @@ const suneStorage={
}
const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}}
const defaultSettings={model:DEFAULT_MODEL,temperature:'',top_p:'',top_k:'',frequency_penalty:'',repetition_penalty:'',min_p:'',top_a:'',verbosity:'',reasoning_effort:'default',system_prompt:'',html:'',extension_html:"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",hide_composer:false,include_thoughts:false,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:'',max_tokens:'',reasoning_effort:'',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||{})})
let sunes=(su.load()||[]).map(makeSune)
const SUNE=window.SUNE=new Proxy({storage:suneStorage,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);markLocalDirty();return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);cleanSuneStorage(id);gcStorage();markLocalDirty();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)$/.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);markLocalDirty()}return true}})
if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)}
const state=window.state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:false}}
const getModelShort=m=>{const mm=m||SUNE.model||'';return mm.includes('/')?mm.split('/').pop():mm}
const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.title=`Settings — ${a.name}`;el.suneBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺';el.footer.classList.toggle('hidden',!!a.settings.hide_composer);await renderSuneHTML();icons()}
const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.title=`Settings — ${a.name}`;el.suneBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-10 w-10 rounded-full object-cover"/>`:'✺';el.footer.classList.toggle('hidden',!!a.settings.hide_composer);await renderSuneHTML();icons()}
const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===SUNE.id?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.id}" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center" title="More"><i data-lucide="more-horizontal" class="h-4 w-4"></i></button></div>`
const renderSidebar=window.renderSidebar=()=>{const list=[...SUNE.list].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()}
const renderUserUI=window.renderUserUI=()=>{if(!el.userMenuAvatar)return;const a=USER.avatar;if(a){el.userMenuAvatar.className='h-6 w-6 rounded-full overflow-hidden shrink-0';el.userMenuAvatar.innerHTML=`<img src="${esc(a)}" class="h-full w-full object-cover"/>`}else{el.userMenuAvatar.className='h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center shrink-0 text-xs';el.userMenuAvatar.textContent='👤'}}
@@ -74,7 +74,7 @@ function msgRow(m){const $row=_createMessageRow(m);$(el.messages).append($row);q
const addMessage=window.addMessage=function(m,track=true){m.id=m.id||gid();if(!Array.isArray(m.content)&&m.content!=null){m.content=[{type:'text',text:String(m.content)}]}const bubble=msgRow(m);bubble.dataset.mid=m.id;renderMarkdown(bubble,partsToText(m));if(track)state.messages.push(m);if(m.role==='assistant')el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:m}}));return bubble}
const addSuneBubbleStreaming=(meta,id)=>msgRow(Object.assign({role:'assistant',id},meta))
const clearChat=()=>{el.suneHtml.dispatchEvent(new CustomEvent('sune:unmount'));state.messages=[];el.messages.innerHTML='';state.attachments=[];updateAttachBadge();el.fileInput.value=''}
const payloadWithSampling=b=>{const o=Object.assign({},b),s=SUNE,p={temperature:num(s.temperature,null),top_p:num(s.top_p,null),top_k:int(s.top_k,null),frequency_penalty:num(s.frequency_penalty,null),repetition_penalty:num(s.repetition_penalty,null),min_p:num(s.min_p,null),top_a:num(s.top_a,null)};Object.keys(p).forEach(k=>{const v=p[k];if(v!==null)o[k]=v});return o}
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),max_tokens:int(s.max_tokens,null)>0?int(s.max_tokens,null):null};Object.keys(p).forEach(k=>{const v=p[k];if(v!==null)o[k]=v});return o}
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.();state.busy=false;setBtnSend()}}
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
function localDemoReply(){return 'Tip: open the sidebar → User to set your API key.'}
@@ -113,15 +113,24 @@ $(el.threadList).on('scroll',()=>{
}
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;const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';if(act==='pin'){th.pinned=!th.pinned;if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}}else if(act==='duplicate'){const newId=gid(),msgs=await localforage.getItem(prefix+th.id)||[];const newTh={...th,id:newId,title:th.title+' (Copy)',updatedAt:Date.now()};if(u.startsWith('gh://'))newTh.status='new';THREAD.list.unshift(newTh);await localforage.setItem(prefix+newId,msgs);await THREAD.save();await renderThreads()}else if(act==='delete'){if(confirm('Delete this chat?')){if(u.startsWith('gh://')){th.status='deleted';th.updatedAt=Date.now()}else{THREAD.list=THREAD.list.filter(x=>!th.id!==th.id);await localforage.removeItem(prefix+th.id)}if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=await localforage.getItem(prefix+th.id)||[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m)||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}else if(act==='export'){const msgs=await localforage.getItem(prefix+th.id)||[];dl(`thread-${(th.title||'thread').replace(/\W/g,'_')}-${ts()}.json`,{...th,messages:msgs})}else if(act==='copy_path'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);if(await copyToClipboard(`${info.owner}/${info.repo}@${info.branch}/${th.id}`))alert('Path copied.')}}hideThreadPopover();await THREAD.save();renderThreads()})
$(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;const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';if(act==='pin'){th.pinned=!th.pinned;if(u.startsWith('gh://')&&th.status!=='new')th.status='modified';}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}}else if(act==='duplicate'){const newId=gid(),msgs=await localforage.getItem(prefix+th.id)||[];const newTh={...th,id:newId,title:th.title+' (Copy)',updatedAt:Date.now()};if(u.startsWith('gh://'))newTh.status='new';THREAD.list.unshift(newTh);await localforage.setItem(prefix+newId,msgs);await THREAD.save();await renderThreads()}else if(act==='delete'){if(confirm('Delete this chat?')){if(u.startsWith('gh://')){th.status='deleted';th.updatedAt=Date.now()}else{THREAD.list=THREAD.list.filter(x=>!th.id!==th.id);await localforage.removeItem(prefix+th.id)}if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=await localforage.getItem(prefix+th.id)||[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m)||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}else if(act==='export'){const msgs=await localforage.getItem(prefix+th.id)||[];dl(`thread-${(th.title||'thread').replace(/\W/g,'_')}-${ts()}.json`,{...th,messages:msgs})}else if(act==='copy_path'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);if(await copyToClipboard(`${info.owner}/${info.repo}@${info.branch}/${th.id}`))alert('Path copied.')}}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();markLocalDirty()};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.sunesSyncUploadBtn).on('click',()=>{hideSunesSyncPopover();performSuneUpload()});
$(el.sunesSyncDownloadBtn).on('click',()=>{hideSunesSyncPopover();performSuneDownload(false)});
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.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(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()})
$(el.composer).on('paste',async e=>{
const cbd=e.clipboardData||e.originalEvent?.clipboardData,files=[...(cbd?.files||[])];
if(!files.length)return;
e.preventDefault();
state.attachments=[];
el.fileInput.value='';
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);state.attachments=[];updateAttachBadge();el.fileInput.value='';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.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)})
let jars={html:null,extension:null};
const ensureJars=async()=>{
if(jars.html&&jars.extension)return jars;
@@ -135,7 +144,7 @@ const ensureJars=async()=>{
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_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_max_tokens.value=s.max_tokens||'';el.set_reasoning_effort.value=s.reasoning_effort||'';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 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==='Script'){openedHTML=true;showHtmlTab('index');ensureJars().then(({html,extension})=>{const s=SUNE.settings;html.updateCode(s.html||'');extension.updateCode(s.extension_html||'')})}}
@@ -146,7 +155,7 @@ $(el.tabModel).on('click',()=>showTab('Model'))
$(el.tabPrompt).on('click',()=>showTab('Prompt'))
$(el.tabScript).on('click',()=>showTab('Script'))
$(el.set_img_output).on('change',e=>el.aspectRatioContainer.classList.toggle('hidden',!e.target.checked))
$(el.settingsForm).on('submit',async e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a'].forEach(k=>SUNE[k]=el[`set_${k}`].value.trim());SUNE.verbosity=(el.set_verbosity.value||'');SUNE.reasoning_effort=(el.set_reasoning_effort.value||'default');SUNE.system_prompt=el.set_system_prompt.value.trim();SUNE.hide_composer=el.set_hide_composer.checked;SUNE.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.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','max_tokens'].forEach(k=>SUNE[k]=el[`set_${k}`].value.trim());SUNE.reasoning_effort=(el.set_reasoning_effort.value||'');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.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

View File

@@ -25,7 +25,7 @@
<form id="settingsForm" class="text-sm">
<div class="border-b flex text-xs font-medium"><button type="button" id="tabModel" class="flex-1 py-2 px-3 text-center border-b-2 border-black">Model & Sampling</button><button type="button" id="tabPrompt" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">System Prompt</button><button type="button" id="tabScript" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">HTML</button></div>
<div id="panelModel" class="p-4 space-y-4">
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3">
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(02)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(01)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
@@ -34,7 +34,7 @@
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(02)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(01)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(01)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Verbosity</label><select id="set_verbosity" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div>
<div><label class="block text-gray-700 font-medium mb-1">Max Tokens (output)</label><input id="set_max_tokens" type="number" min="1" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder=""/></div>
</div>
<div class="flex flex-wrap items-center gap-2 pt-2">
<div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div>
@@ -70,6 +70,7 @@
</div>
<div id="panelPrompt" class="p-4 space-y-4 hidden">
<div><div class="flex items-center justify-between mb-1"><label for="set_system_prompt" class="block text-gray-700 font-medium">System Prompt</label><div class="flex gap-2"><button type="button" id="copySystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteSystemPrompt" class="px-2 py-1 text-xs rounded-md bg-gray-100 hover:bg-gray-200">Paste</button></div></div><textarea id="set_system_prompt" rows="8" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Enter a system prompt to guide the sune"></textarea></div>
<p class="text-xs text-gray-500">Use <code>{{model_name}}</code> to insert the model name.</p>
</div>
<div id="panelScript" class="p-1 hidden">
<div class="border-b flex text-xs font-medium"><button type="button" id="htmlTab_index" class="flex-1 py-2 px-3 text-center border-b-2"></button><button type="button" id="htmlTab_extension" class="flex-1 py-2 px-3 text-center border-b-2"></button></div>

View File

@@ -1,7 +1,7 @@
<header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center">
<div class="mx-auto w-full max-w-none px-4 py-2 grid grid-cols-3 items-center">
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
<button id="suneBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings"></button>
<button id="suneBtnTop" class="justify-self-center h-10 w-10 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings"></button>
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
</div>
</header>

View File

@@ -9,7 +9,8 @@ export const buildBody=()=>{
msgs.push({role:'system', content: mPrompt});
}
const sPrompt = (SUNE.system_prompt || '').trim();
const modelName=SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/,'').replace(/^[^/]*\//,'').replace(/(?::online|:free)+$/,'');
const sPrompt = (SUNE.system_prompt || '').trim().replaceAll('{{model_name}}',()=>modelName);
if(sPrompt) {
msgs.push({role:'system', content: sPrompt});
}
@@ -38,7 +39,6 @@ export const buildBody=()=>{
const b=payloadWithSampling({model:SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/,''),messages:msgs,stream:true});
b.reasoning={...(SUNE.reasoning_effort&&SUNE.reasoning_effort!=='default'?{effort:SUNE.reasoning_effort}:{}),exclude:!SUNE.include_thoughts};
if(SUNE.verbosity)b.verbosity=SUNE.verbosity;
if(SUNE.img_output){b.modalities=['image'];b.image_config={aspect_ratio:SUNE.aspect_ratio||'1:1',image_size:SUNE.image_size||'1K'}}
return b
}

View File

@@ -16,7 +16,7 @@ export const fmtSize = b => {
return (x >= 10 ? Math.round(x) : Math.round(x * 10) / 10) + ' ' + u[i];
};
export const asDataURL = f => new Promise(r => { const fr = new FileReader(); fr.onload = () => r(String(fr.result || '')); fr.readAsDataURL(f); });
export const imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => {
export const imgToWebp = (f, D = 96, q = 75) => new Promise((r, j) => {
if (!f) return j();
const i = new Image;
i.onload = () => {