This build was committed by a bot.

This commit is contained in:
github-actions
2025-08-23 15:51:59 +00:00
parent de0baf90c7
commit aae57e2143
9 changed files with 10 additions and 266 deletions

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
sune.planetrenox.com

View File

@@ -21,7 +21,7 @@
<link rel="icon" type="image/avif" href="https://sune.planetrenox.com/✺.avif"> <link rel="icon" type="image/avif" href="https://sune.planetrenox.com/✺.avif">
<script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script> <script src="https://cdn.jsdelivr.net/npm/tiny-ripple@0.2.0"></script>
<style>:root{--safe-bottom:env(safe-area-inset-bottom)}::-webkit-scrollbar{height:8px;width:8px}::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}</style> <style>:root{--safe-bottom:env(safe-area-inset-bottom)}::-webkit-scrollbar{height:8px;width:8px}::-webkit-scrollbar-thumb{background:#e5e7eb;border-radius:999px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}</style>
<link rel="manifest" href="/devsune/manifest.webmanifest"><script id="vite-plugin-pwa:register-sw" src="/devsune/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" hx-on="click: if(!document.getElementById('historyMenu').contains(event.target)&&!event.target.closest('[data-thread-menu]')) hideHistoryMenu(); if(!document.getElementById('suneMenu').contains(event.target)&&!event.target.closest('[data-sune-menu]')) hideSuneMenu(); 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" hx-on="click: if(!document.getElementById('historyMenu').contains(event.target)&&!event.target.closest('[data-thread-menu]')) hideHistoryMenu(); if(!document.getElementById('suneMenu').contains(event.target)&&!event.target.closest('[data-sune-menu]')) hideSuneMenu(); 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"> <div class="flex flex-col h-dvh max-h-dvh">
<header class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200"> <header class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">

View File

@@ -1 +1 @@
{"name":"Sune","short_name":"Sune","description":"OpenRouter GUI Frontend","start_url":"/devsune/","display":"standalone","background_color":"#000000","theme_color":"#FFFFFF","lang":"en","scope":"/devsune/","id":"/devsune/","orientation":"portrait","categories":["productivity","utilities"],"icons":[{"src":"https://sune.planetrenox.com/appstore_content/✺.png","sizes":"1024x1024","type":"image/png"}],"screenshots":[{"src":"https://sune.planetrenox.com/appstore_content/screenshot1.jpg","sizes":"1344x2693","type":"image/jpeg"},{"src":"https://sune.planetrenox.com/appstore_content/screenshot2.jpg","sizes":"1344x2699","type":"image/jpeg"}]} {"name":"Sune","short_name":"Sune","description":"OpenRouter GUI Frontend","start_url":"https://sune.planetrenox.com/","display":"standalone","background_color":"#000000","theme_color":"#FFFFFF","lang":"en","scope":"/","id":"https://sune.planetrenox.com/","orientation":"portrait","categories":["productivity","utilities"],"icons":[{"src":"https://sune.planetrenox.com/appstore_content/✺.png","sizes":"1024x1024","type":"image/png"}],"screenshots":[{"src":"https://sune.planetrenox.com/appstore_content/screenshot1.jpg","sizes":"1344x2693","type":"image/jpeg"},{"src":"https://sune.planetrenox.com/appstore_content/screenshot2.jpg","sizes":"1344x2699","type":"image/jpeg"}]}

View File

@@ -1 +1 @@
if('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('/devsune/sw.js', { scope: '/devsune/' })})} if('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('/sw.js', { scope: '/' })})}

View File

@@ -1,140 +1 @@
self.importScripts("https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"); if(!self.define){let e,i={};const t=(t,n)=>(t=new URL(t+".js",n).href,i[t]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=t,e.onload=i,document.head.appendChild(e)}else e=t,importScripts(t),i()}).then(()=>{let e=i[t];if(!e)throw new Error(`Module ${t} didnt register its module`);return e}));self.define=(n,r)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let o={};const c=e=>t(e,s),d={module:{uri:s},exports:o,require:c};i[s]=Promise.all(n.map(e=>d[e]||c(e))).then(e=>(r(...e),o))}}define(["./workbox-5ffe50d4"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"91527abb70887d6e7c52a125ccf0f5fd"},{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
localforage.config({ name: "localforage", storeName: "keyvaluepairs" });
const TKEY = "threads_v1";
const now = () => Date.now();
const gid = () => Math.random().toString(36).slice(2, 9);
const titleFrom = (t) => String(t || "").replace(/\s+/g, " ").trim().slice(0, 60) || "Untitled";
async function loadThreads() {
const v = await localforage.getItem(TKEY);
return Array.isArray(v) ? v : [];
}
async function saveThreads(v) {
await localforage.setItem(TKEY, v);
}
async function pickLatestThread(threads) {
if (!threads.length) return null;
let best = threads[0], bu = +best.updatedAt || 0;
for (const t of threads) {
const u = +t.updatedAt || 0;
if (u > bu) {
best = t;
bu = u;
}
}
return best;
}
async function ensureThreadForWrite(reqMeta) {
let threads = await loadThreads();
let th = await pickLatestThread(threads);
if (!th) {
const id = gid();
const firstUser = (reqMeta.messages || []).find((m) => m && m.role === "user")?.content || "";
th = { id, title: titleFrom(firstUser), pinned: false, updatedAt: now(), messages: [] };
threads.unshift(th);
await saveThreads(threads);
}
return th.id;
}
async function appendAssistantPlaceholder(threadId, meta) {
let threads = await loadThreads();
let th = threads.find((t) => t.id === threadId);
if (!th) {
th = { id: threadId, title: "Untitled", pinned: false, updatedAt: now(), messages: [] };
threads.unshift(th);
}
const mid = "sw_" + gid();
const msg = { id: mid, role: "assistant", content: "", sune_name: "", model: meta?.model || "", avatar: "", sw: true };
th.messages = [...Array.isArray(th.messages) ? th.messages : [], msg];
th.updatedAt = now();
await saveThreads(threads);
return mid;
}
async function updateAssistantContent(threadId, mid, content) {
let threads = await loadThreads();
const th = threads.find((t) => t.id === threadId);
if (!th) return;
const i = (th.messages || []).findIndex((m) => m && m.id === mid);
if (i < 0) return;
th.messages[i].content = content;
th.updatedAt = now();
await saveThreads(threads);
}
async function parseAndPersist(stream, threadId, mid) {
const reader = stream.getReader();
const dec = new TextDecoder("utf-8");
let buf = "", full = "", lastWrite = 0;
const flush = async (force = false) => {
const t = Date.now();
if (force || t - lastWrite > 250) {
await updateAssistantContent(threadId, mid, full);
lastWrite = t;
}
};
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let idx;
while ((idx = buf.indexOf("\n\n")) !== -1) {
const chunk = buf.slice(0, idx).trim();
buf = buf.slice(idx + 2);
if (!chunk) continue;
if (chunk.startsWith("data:")) {
const data = chunk.slice(5).trim();
if (data === "[DONE]") {
await flush(true);
return;
}
try {
const json = JSON.parse(data);
const d = json && json.choices && json.choices[0] && json.choices[0].delta && json.choices[0].delta.content || "";
if (d) {
full += d;
await flush(false);
}
} catch (_) {
}
}
}
}
await flush(true);
}
async function handleOpenRouterEvent(event) {
const req = event.request;
const url = new URL(req.url);
const isTarget = url.hostname === "openrouter.ai" && /\/api\/v1\/chat\/completions$/.test(url.pathname);
if (!isTarget) return fetch(req);
let reqMeta = {};
try {
const t = await req.clone().text();
reqMeta = JSON.parse(t || "{}");
} catch (_) {
}
if (!reqMeta || !reqMeta.stream) {
return fetch(req);
}
const res = await fetch(req);
if (!res.body) return res;
const [toClient, toTap] = res.body.tee();
event.waitUntil((async () => {
try {
const msgs = Array.isArray(reqMeta.messages) ? reqMeta.messages : [];
const meta = { sune_name: "", model: reqMeta.model || "", avatar: "" };
const threadId = await ensureThreadForWrite({ messages: msgs });
const mid = await appendAssistantPlaceholder(threadId, meta);
await parseAndPersist(toTap, threadId, mid);
} catch (_) {
}
})());
const headers = new Headers(res.headers);
return new Response(toClient, { status: res.status, statusText: res.statusText, headers });
}
self.addEventListener("install", (e) => {
self.skipWaiting();
});
self.addEventListener("activate", (e) => {
e.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", (e) => {
e.respondWith(handleOpenRouterEvent(e));
});

1
docs/workbox-5ffe50d4.js Normal file

File diff suppressed because one or more lines are too long

1
public/CNAME Normal file
View File

@@ -0,0 +1 @@
sune.planetrenox.com

View File

@@ -1,111 +0,0 @@
self.importScripts('https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js')
localforage.config({name:'localforage',storeName:'keyvaluepairs'})
const TKEY='threads_v1'
const now=()=>Date.now()
const gid=()=>Math.random().toString(36).slice(2,9)
const titleFrom=t=>(String(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled')
async function loadThreads(){const v=await localforage.getItem(TKEY);return Array.isArray(v)?v:[]}
async function saveThreads(v){await localforage.setItem(TKEY,v)}
async function pickLatestThread(threads){if(!threads.length)return null;let best=threads[0],bu=+best.updatedAt||0;for(const t of threads){const u=+t.updatedAt||0;if(u>bu){best=t;bu=u}}return best}
async function ensureThreadForWrite(reqMeta){
let threads=await loadThreads()
let th=await pickLatestThread(threads)
if(!th){
const id=gid()
const firstUser=(reqMeta.messages||[]).find(m=>m&&m.role==='user')?.content||''
th={id,title:titleFrom(firstUser),pinned:false,updatedAt:now(),messages:[]}
threads.unshift(th)
await saveThreads(threads)
}
return th.id
}
async function appendAssistantPlaceholder(threadId,meta){
let threads=await loadThreads()
let th=threads.find(t=>t.id===threadId)
if(!th){th={id:threadId,title:'Untitled',pinned:false,updatedAt:now(),messages:[]};threads.unshift(th)}
const mid='sw_'+gid()
const msg={id:mid,role:'assistant',content:'',sune_name:meta?.sune_name||'',model:meta?.model||'',avatar:meta?.avatar||'',sw:true}
th.messages=[...(Array.isArray(th.messages)?th.messages:[]),msg]
th.updatedAt=now()
await saveThreads(threads)
return mid
}
async function updateAssistantContent(threadId,mid,content){
let threads=await loadThreads()
const th=threads.find(t=>t.id===threadId)
if(!th)return
const i=(th.messages||[]).findIndex(m=>m&&m.id===mid)
if(i<0)return
th.messages[i].content=content
th.updatedAt=now()
await saveThreads(threads)
}
async function parseAndPersist(stream,threadId,mid){
const reader=stream.getReader()
const dec=new TextDecoder('utf-8')
let buf='',full='',lastWrite=0
const flush=async(force=false)=>{const t=Date.now();if(force||t-lastWrite>250){await updateAssistantContent(threadId,mid,full);lastWrite=t}}
while(true){
const {value,done}=await reader.read()
if(done)break
buf+=dec.decode(value,{stream:true})
let idx
while((idx=buf.indexOf('\n\n'))!==-1){
const chunk=buf.slice(0,idx).trim()
buf=buf.slice(idx+2)
if(!chunk)continue
if(chunk.startsWith('data:')){
const data=chunk.slice(5).trim()
if(data==='[DONE]'){await flush(true);return}
try{
const json=JSON.parse(data)
const d=json&&json.choices&&json.choices[0]&&json.choices[0].delta&&json.choices[0].delta.content||''
if(d){full+=d;await flush(false)}
}catch(_){}
}
}
}
await flush(true)
}
async function handleOpenRouterEvent(event){
const req=event.request
const url=new URL(req.url)
const isTarget=url.hostname==='openrouter.ai'&&/\/api\/v1\/chat\/completions$/.test(url.pathname)
if(!isTarget)return fetch(req)
let reqMeta={}
try{const t=await req.clone().text();reqMeta=JSON.parse(t||'{}')}catch(_){}
if(!reqMeta||!reqMeta.stream){return fetch(req)}
const res=await fetch(req)
if(!res.body)return res
const [toClient,toTap]=res.body.tee()
event.waitUntil((async()=>{
try{
const msgs=Array.isArray(reqMeta.messages)?reqMeta.messages:[]
const meta={sune_name:'',model:reqMeta.model||'',avatar:''}
const threadId=await ensureThreadForWrite({messages:msgs})
const mid=await appendAssistantPlaceholder(threadId,meta)
await parseAndPersist(toTap,threadId,mid)
}catch(_){}
})())
const headers=new Headers(res.headers)
return new Response(toClient,{status:res.status,statusText:res.statusText,headers})
}
self.addEventListener('install',e=>{self.skipWaiting()})
self.addEventListener('activate',e=>{e.waitUntil(self.clients.claim())})
self.addEventListener('fetch',e=>{e.respondWith(handleOpenRouterEvent(e))})

View File

@@ -3,18 +3,13 @@ import { VitePWA } from 'vite-plugin-pwa'
import { createHtmlPlugin } from 'vite-plugin-html' import { createHtmlPlugin } from 'vite-plugin-html'
const pwa = VitePWA({ const pwa = VitePWA({
base: '/devsune/',
strategies: 'injectManifest',
registerType: 'autoUpdate', registerType: 'autoUpdate',
injectManifest: { injectionPoint: undefined },
devOptions: { enabled: true },
manifest: { manifest: {
id: '/devsune/', id: 'https://sune.planetrenox.com/',
name: 'Sune', name: 'Sune',
short_name: 'Sune', short_name: 'Sune',
description: 'OpenRouter GUI Frontend', description: 'OpenRouter GUI Frontend',
start_url: '/devsune/', start_url: 'https://sune.planetrenox.com/',
scope: '/devsune/',
display: 'standalone', display: 'standalone',
orientation: 'portrait', orientation: 'portrait',
theme_color: '#FFFFFF', theme_color: '#FFFFFF',
@@ -41,8 +36,4 @@ const html = createHtmlPlugin({
} }
}) })
export default defineConfig({ export default defineConfig({ build: { outDir: 'docs', minify: false }, plugins: [pwa, html] })
base: '/devsune/',
build: { outDir: 'docs', minify: false },
plugins: [pwa, html]
})