11 Commits

Author SHA1 Message Date
github-actions[bot]
d3e56d2b88 This build was committed by a bot. 2026-09-18 18:52:27 +00:00
081074a2df Invalidate stale GitHub thread caches on pull 2026-09-18 11:52:04 -07:00
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
9 changed files with 186 additions and 40 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", role: "system",
content: mPrompt 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({ if (sPrompt) msgs.push({
role: "system", role: "system",
content: sPrompt content: sPrompt
@@ -645,6 +646,11 @@ function kbUpdate() {
el.chat.style.scrollPaddingBottom = fh + overlap + 16 + "px"; el.chat.style.scrollPaddingBottom = fh + overlap + 16 + "px";
} }
function kbBind() { 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 })); if (window.visualViewport) ["resize", "scroll"].forEach((ev) => window.visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true }));
window.$(window).on("resize orientationchange", () => setTimeout(kbUpdate, 50)); window.$(window).on("resize orientationchange", () => setTimeout(kbUpdate, 50));
window.$(el.input).on("focus click", () => { window.$(el.input).on("focus click", () => {
@@ -1736,7 +1742,6 @@ function updateAttachBadge() {
el.attachBadge.classList.toggle("hidden", n === 0); el.attachBadge.classList.toggle("hidden", n === 0);
} }
$(el.attachBtn).on("click", () => { $(el.attachBtn).on("click", () => {
if (state.busy) return;
if (state.attachments.length) { if (state.attachments.length) {
state.attachments = []; state.attachments = [];
updateAttachBadge(); updateAttachBadge();
@@ -1753,6 +1758,18 @@ $(el.fileInput).on("change", async () => {
} }
updateAttachBadge(); 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) => { $(el.composer).on("submit", async (e) => {
e.preventDefault(); e.preventDefault();
if (state.busy) return; if (state.busy) return;
@@ -1767,6 +1784,9 @@ $(el.composer).on("submit", async (e) => {
text text
}); });
parts.push(...state.attachments); parts.push(...state.attachments);
state.attachments = [];
updateAttachBadge();
el.fileInput.value = "";
const userMsg = { const userMsg = {
role: "user", role: "user",
content: parts.length ? parts : [{ 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"; 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); await THREAD.setTitle(th.id, title);
})(); })();
if (!SUNE.model) return state.attachments = [], updateAttachBadge(); if (!SUNE.model) return;
state.busy = true; state.busy = true;
setBtnStop(); setBtnStop();
const a = SUNE.active, suneMeta = { const a = SUNE.active, suneMeta = {
@@ -1834,8 +1854,6 @@ $(el.composer).on("submit", async (e) => {
} else if (!done) THREAD.persist(false); } else if (!done) THREAD.persist(false);
}; };
await streamChat(onDelta, streamId); await streamChat(onDelta, streamId);
state.attachments = [];
updateAttachBadge();
}); });
var jars = { var jars = {
html: null, html: null,
@@ -2150,40 +2168,44 @@ el.htmlTab_index.textContent = "index.html";
el.htmlTab_extension.textContent = "extension.html"; el.htmlTab_extension.textContent = "extension.html";
el.htmlTab_index.onclick = () => showHtmlTab("index"); el.htmlTab_index.onclick = () => showHtmlTab("index");
el.htmlTab_extension.onclick = () => showHtmlTab("extension"); el.htmlTab_extension.onclick = () => showHtmlTab("extension");
var pullThreads = async () => { var pullThreads = async (invalidateAll = false) => {
const u = el.threadRepoInput.value.trim(); const u = el.threadRepoInput.value.trim();
if (!u.startsWith("gh://")) return; if (!u.startsWith("gh://")) return;
const info = parseGhUrl(u); const info = parseGhUrl(u);
try { try {
const items = await ghApi(`${info.apiPath}?ref=${info.branch}`); const items = await ghApi(`${info.apiPath}?ref=${info.branch}`);
if (!items) { const local = await localforage.getItem("rem_index_" + u.substring(5)) || [];
THREAD.list = []; const remote = (items || []).map((i) => {
await THREAD.save(); if (i.type === "dir") return {
} else { id: i.name,
THREAD.list = items.map((i) => { title: i.name,
if (i.type === "dir") return { type: "folder",
id: i.name, updatedAt: 0
title: i.name, };
type: "folder", if (i.type === "file" && i.name.endsWith(".md")) return {
updatedAt: 0 id: i.path,
}; title: i.name,
if (i.type === "file" && i.name.endsWith(".md")) return { type: "file",
id: i.path, updatedAt: 0
title: i.name, };
type: "file", const d = deserializeThreadName(i.name);
updatedAt: 0 return d ? {
}; ...d,
const d = deserializeThreadName(i.name); status: "synced"
return d ? { } : null;
...d, }).filter(Boolean);
status: "synced" for (const t of invalidateAll ? [...local, ...remote] : local) {
} : null; if (t.type !== "thread") continue;
}).filter(Boolean); const next = remote.find((r) => r.type === "thread" && r.id === t.id);
await THREAD.save(); if (invalidateAll || t.status !== "synced" || !next || next.updatedAt > t.updatedAt) await localforage.removeItem("rem_t_" + t.id);
} }
THREAD.list = remote;
await THREAD.save();
await renderThreads(); await renderThreads();
return true;
} catch (e) { } catch (e) {
console.error("Auto-pull failed:", e); console.error("Auto-pull failed:", e);
return false;
} }
}; };
$(el.threadRepoInput).on("change", async () => { $(el.threadRepoInput).on("change", async () => {
@@ -2283,8 +2305,9 @@ $(el.threadSyncBtn).on("click", async () => {
THREAD.list = THREAD.list.filter((x) => !toRemove.includes(x.id)); THREAD.list = THREAD.list.filter((x) => !toRemove.includes(x.id));
await THREAD.save(); await THREAD.save();
alert("Pushed to GitHub."); alert("Pushed to GitHub.");
} else { } else if (await pullThreads(true)) {
await pullThreads(); state.currentThreadId = null;
clearChat();
alert("Pulled from GitHub."); alert("Pulled from GitHub.");
} }
await renderThreads(); await renderThreads();

3
dist/index.html vendored
View File

@@ -13,7 +13,7 @@
<script defer src="//unpkg.com/alpinejs"></script> <script defer src="//unpkg.com/alpinejs"></script>
<script type="module" crossorigin src="/assets/index-DhMktBAg.js"></script> <script type="module" crossorigin src="/assets/index-BqgZBjfh.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DUC1RW1F.css"> <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> <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')">
@@ -151,6 +151,7 @@
</div> </div>
<div id="panelPrompt" class="p-4 space-y-4 hidden"> <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> <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>
<div id="panelScript" class="p-1 hidden"> <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> <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,t)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(s[r])return;let o={};const l=e=>i(e,r),c={module:{uri:r},exports:o,require:l};s[r]=Promise.all(n.map(e=>c[e]||l(e))).then(e=>(t(...e),o))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"index.html",revision:"7fcb86f70380b572d99952f53cabf640"},{url:"assets/index-DhMktBAg.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 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:"f47c1e8b7a8062c7e3289f887383a9fa"},{url:"assets/index-DUC1RW1F.css",revision:null},{url:"assets/index-BqgZBjfh.js",revision:null},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});

View File

@@ -11,6 +11,11 @@ export function kbUpdate() {
} }
export function kbBind() { 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) { if (window.visualViewport) {
['resize', 'scroll'].forEach(ev => window.visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true })); ['resize', 'scroll'].forEach(ev => window.visualViewport.addEventListener(ev, () => kbUpdate(), { passive: true }));
} }

View File

@@ -119,9 +119,18 @@ $(el.sunePopover).on('click',async e=>{const act=e.target.closest('[data-action]
$(el.sunesSyncUploadBtn).on('click',()=>{hideSunesSyncPopover();performSuneUpload()}); $(el.sunesSyncUploadBtn).on('click',()=>{hideSunesSyncPopover();performSuneUpload()});
$(el.sunesSyncDownloadBtn).on('click',()=>{hideSunesSyncPopover();performSuneDownload(false)}); $(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)} 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.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}; let jars={html:null,extension:null};
const ensureJars=async()=>{ const ensureJars=async()=>{
if(jars.html&&jars.extension)return jars; if(jars.html&&jars.extension)return jars;
@@ -192,11 +201,32 @@ $(document).on('click',e=>{if(el.sunesSyncPopover&&!el.sunesSyncPopover.classLis
const htmlTabs={index:['htmlTab_index','htmlEditor'],extension:['htmlTab_extension','extensionHtmlEditor']};function showHtmlTab(key){Object.entries(htmlTabs).forEach(([k,[tb,pn]])=>{const a=k===key;el[tb].classList.toggle('border-black',a);el[tb].classList.toggle('border-transparent',!a);el[tb].classList.toggle('hover:border-gray-300',!a);el[pn].classList.toggle('hidden',!a)})} const htmlTabs={index:['htmlTab_index','htmlEditor'],extension:['htmlTab_extension','extensionHtmlEditor']};function showHtmlTab(key){Object.entries(htmlTabs).forEach(([k,[tb,pn]])=>{const a=k===key;el[tb].classList.toggle('border-black',a);el[tb].classList.toggle('border-transparent',!a);el[tb].classList.toggle('hover:border-gray-300',!a);el[pn].classList.toggle('hidden',!a)})}
el.htmlTab_index.textContent='index.html';el.htmlTab_extension.textContent='extension.html'; el.htmlTab_index.textContent='index.html';el.htmlTab_extension.textContent='extension.html';
el.htmlTab_index.onclick=()=>showHtmlTab('index');el.htmlTab_extension.onclick=()=>showHtmlTab('extension'); el.htmlTab_index.onclick=()=>showHtmlTab('index');el.htmlTab_extension.onclick=()=>showHtmlTab('extension');
const pullThreads=async()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const info=parseGhUrl(u);try{const items=await ghApi(`${info.apiPath}?ref=${info.branch}`);if(!items){THREAD.list=[];await THREAD.save()}else{THREAD.list=items.map(i=>{if(i.type==='dir')return {id:i.name,title:i.name,type:'folder',updatedAt:0};if(i.type==='file'&&i.name.endsWith('.md'))return {id:i.path,title:i.name,type:'file',updatedAt:0};const d=deserializeThreadName(i.name);return d?{...d,status:'synced'}:null}).filter(Boolean);await THREAD.save()}await renderThreads()}catch(e){console.error('Auto-pull failed:',e)}}; const pullThreads=async(invalidateAll=false)=>{
const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;
const info=parseGhUrl(u);
try{
const items=await ghApi(`${info.apiPath}?ref=${info.branch}`);
const local=await localforage.getItem('rem_index_'+u.substring(5))||[];
const remote=(items||[]).map(i=>{
if(i.type==='dir')return {id:i.name,title:i.name,type:'folder',updatedAt:0};
if(i.type==='file'&&i.name.endsWith('.md'))return {id:i.path,title:i.name,type:'file',updatedAt:0};
const d=deserializeThreadName(i.name);return d?{...d,status:'synced'}:null;
}).filter(Boolean);
for(const t of invalidateAll?[...local,...remote]:local){
if(t.type!=='thread')continue;
const next=remote.find(r=>r.type==='thread'&&r.id===t.id);
if(invalidateAll||t.status!=='synced'||!next||next.updatedAt>t.updatedAt)await localforage.removeItem('rem_t_'+t.id);
}
THREAD.list=remote;
await THREAD.save();
await renderThreads();
return true;
}catch(e){console.error('Auto-pull failed:',e);return false}
};
$(el.threadRepoInput).on('change',async()=>{const u=el.threadRepoInput.value.trim();localStorage.setItem('thread_repo_url',u);if(state.currentThreadId){state.currentThreadId=null;clearChat()}el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);if(u.startsWith('gh://'))await pullThreads();else{await THREAD.load();await renderThreads()}}); $(el.threadRepoInput).on('change',async()=>{const u=el.threadRepoInput.value.trim();localStorage.setItem('thread_repo_url',u);if(state.currentThreadId){state.currentThreadId=null;clearChat()}el.threadFolderBtn.classList.toggle('hidden',!u.startsWith('gh://'));el.threadBackBtn.classList.toggle('hidden',!u.startsWith('gh://')||u.split('/').length<=3);if(u.startsWith('gh://'))await pullThreads();else{await THREAD.load();await renderThreads()}});
$(el.threadBackBtn).on('click',()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const p=u.split('/');if(p.length>3){p.pop();el.threadRepoInput.value=p.join('/');el.threadRepoInput.dispatchEvent(new Event('change'))}}); $(el.threadBackBtn).on('click',()=>{const u=el.threadRepoInput.value.trim();if(!u.startsWith('gh://'))return;const p=u.split('/');if(p.length>3){p.pop();el.threadRepoInput.value=p.join('/');el.threadRepoInput.dispatchEvent(new Event('change'))}});
$(el.threadFolderBtn).on('click',async()=>{const n=prompt('Folder name:');if(!n)return;THREAD.list.unshift({id:n.trim(),title:n.trim(),type:'folder',updatedAt:Date.now()});await THREAD.save();await renderThreads()}); $(el.threadFolderBtn).on('click',async()=>{const n=prompt('Folder name:');if(!n)return;THREAD.list.unshift({id:n.trim(),title:n.trim(),type:'folder',updatedAt:Date.now()});await THREAD.save();await renderThreads()});
$(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);let msgs=await localforage.getItem('rem_t_'+t.id);if((!msgs||!Array.isArray(msgs))&&remoteMap[t.id]){const text=await ghGetFileContent(info,remoteMap[t.id].name);if(text){try{msgs=JSON.parse(text);await localforage.setItem('rem_t_'+t.id,msgs)}catch(e){console.error(e)}}}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);let msgs=await localforage.getItem('rem_t_'+t.id);if((!msgs||!Array.isArray(msgs))&&remoteMap[t.id]){const text=await ghGetFileContent(info,remoteMap[t.id].name);if(text){try{msgs=JSON.parse(text);await localforage.setItem('rem_t_'+t.id,msgs)}catch(e){console.error(e)}}}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 if(await pullThreads(true)){state.currentThreadId=null;clearChat();alert('Pulled from GitHub.')}await renderThreads()}catch(e){alert('Sync failed: '+e.message)}});
let suneSyncBusy=false; let suneSyncBusy=false;
const checkSuneSyncStatus=async()=>{ const checkSuneSyncStatus=async()=>{

View File

@@ -70,6 +70,7 @@
</div> </div>
<div id="panelPrompt" class="p-4 space-y-4 hidden"> <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> <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>
<div id="panelScript" class="p-1 hidden"> <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> <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

@@ -9,7 +9,8 @@ export const buildBody=()=>{
msgs.push({role:'system', content: mPrompt}); 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) { if(sPrompt) {
msgs.push({role:'system', content: sPrompt}); msgs.push({role:'system', content: sPrompt});
} }