Files
sune/index.html
2025-09-04 10:21:11 -07:00

207 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.5.1/github-markdown-light.min.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"/>
<style>
.markdown-body{font-size:14px;line-height:1.6}.markdown-body pre{overflow:auto}
.msg-bubble{overflow-x:auto}
.copy-btn{position:absolute;top:.5rem;right:.5rem;background:#0f172a;color:#fff;border-radius:.5rem;padding:.25rem .5rem;font-size:12px;opacity:.85}
.msg-avatar{font-size:16px}
.menu-card{position:fixed;z-index:60;min-width:12rem;border-radius:0.75rem;border:1px solid #e5e7eb;background:#fff;box-shadow:0 10px 20px rgba(0,0,0,.08)}
.menu-item{width:100%;text-align:left;padding:.5rem .75rem;font-size:0.875rem;display:flex;align-items:center;gap:.5rem}
#htmlEditor,#extensionHtmlEditor{outline:none;white-space:pre!important;font-size:11px;line-height:1.5;}
</style>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body class="bg-white text-gray-900 selection:bg-black/10" hx-on="click: 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">
<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">
<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" hx-on="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>
<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" hx-on="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>
<main id="chat" class="flex-1 overflow-y-auto no-scrollbar"><section id="suneHtml" class="px-1 border-b border-gray-200 hidden"></section><div id="messages" class="mx-auto w-full max-w-none px-0 py-4 sm:py-6 space-y-4" hx-on="click: if(event.target.closest('.msg-avatar')){document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')}"></div><div class="h-24"></div></main>
<footer id="footer" class="sticky bottom-0 z-10 bg-gradient-to-t from-white via-white/95 to-white/40 pt-2 pb-[calc(12px+var(--safe-bottom))] border-t border-gray-200">
<div class="mx-auto w-full max-w-none px-0">
<form id="composer" class="group relative flex items-start gap-2 px-3">
<textarea id="input" rows="1" placeholder="Send a message" spellcheck="false" autocapitalize="none" autocomplete="off" autocorrect="off" inputmode="text" enterkeyhint="enter" class="flex-1 resize-none rounded-2xl border-none bg-white px-3 py-2 text-[14px] leading-6 placeholder:text-gray-400 focus:outline-none focus:ring-0 max-h-52 overflow-y-auto min-h-[96px]"></textarea>
<div class="flex flex-col gap-2 self-stretch justify-center">
<button id="sendBtn" type="submit" aria-label="Send" class="shrink-0 rounded-2xl bg-black text-white h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-black/90 active:scale-[.98] transition"><i data-lucide="sparkles" class="h-5 w-5"></i></button>
<button id="attachBtn" type="button" aria-label="Attach" class="relative shrink-0 rounded-2xl bg-gray-100 text-gray-900 h-10 w-10 inline-flex items-center justify-center shadow-sm hover:bg-gray-200 active:scale-[.98] transition"><i data-lucide="paperclip" class="h-5 w-5"></i><span id="attachBadge" class="hidden absolute -top-1 -right-1 h-4 min-w-4 px-1 rounded-full bg-black text-white text-[10px] leading-4 text-center"></span></button>
</div>
<input id="fileInput" type="file" class="hidden" multiple accept="image/png,image/jpeg,image/webp,image/gif,application/pdf,audio/wav,audio/x-wav,audio/mpeg,audio/mp3"/>
</form>
</div>
</footer>
</div>
<div id="sidebarOverlayLeft" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:document.getElementById('sidebarLeft').classList.add('-translate-x-full');this.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full');document.getElementById('sidebarOverlayRight').classList.add('hidden');hideThreadPopover();hideSunePopover()"></div>
<aside id="sidebarLeft" class="fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] bg-white border-r border-gray-200 shadow-xl transform -translate-x-full transition-transform duration-200 ease-out flex flex-col">
<div class="p-3 border-b flex items-center gap-2"><button id="newSuneBtn" class="px-3 py-2 rounded-xl bg-black text-white text-sm hover:bg-black/90">New sune</button><span class="text-xs text-gray-500">Click name to equip</span></div>
<div id="suneList" class="flex-1 overflow-y-auto divide-y"></div>
<div class="p-3 border-t relative">
<button id="userMenuBtn" class="w-full flex items-center justify-between px-3 py-2 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition" hx-on="click:event.stopPropagation();document.getElementById('userMenu').classList.toggle('hidden')"><span class="flex items-center gap-2"><span class="h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center">👤</span><span class="text-sm">Account & Backup</span></span><i data-lucide="chevron-down" class="h-4 w-4"></i></button>
<div id="userMenu" class="absolute left-3 right-3 bottom-16 translate-y-2 rounded-xl border border-gray-200 bg-white shadow-lg hidden overflow-hidden">
<button id="accountSettingsOption" class="menu-item"><i data-lucide="settings" class="h-4 w-4"></i><span>Settings</span></button>
<button id="sunesImportOption" class="menu-item">Import sunes (.sune)</button>
<button id="sunesExportOption" class="menu-item">Export sunes (.sune)</button>
<button id="threadsImportOption" class="menu-item">Import threads (.json)</button>
<button id="threadsExportOption" class="menu-item">Export threads (.json)</button>
</div>
<input id="importInput" type="file" accept="application/json,.json,.sune" class="hidden"/>
</div>
</aside>
<div id="sidebarOverlayRight" class="fixed inset-0 z-40 bg-black/20 hidden" hx-on="click:this.classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full')"></div>
<aside id="sidebarRight" class="fixed inset-y-0 right-0 z-50 w-80 max-w-[90vw] bg-white border-l border-gray-200 shadow-xl transform translate-x-full transition-transform duration-200 ease-out flex flex-col">
<div class="p-3 border-b text-sm font-medium flex items-center justify-between"><span>Threads</span><button id="closeThreads" class="p-1 rounded hover:bg-gray-100" aria-label="Close" hx-on="click:document.getElementById('sidebarOverlayRight').classList.add('hidden');document.getElementById('sidebarRight').classList.add('translate-x-full')"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
<div id="threadList" class="flex-1 overflow-y-auto divide-y"></div>
</aside>
<div id="threadPopover" class="menu-card hidden">
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
<button data-action="delete" class="menu-item text-red-600"><i data-lucide="trash-2" class="h-4 w-4"></i><span>Delete</span></button>
<button data-action="count_tokens" class="menu-item"><i data-lucide="hash" class="h-4 w-4"></i><span>Count tokens (approx.)</span></button>
</div>
<div id="sunePopover" class="menu-card hidden">
<button data-action="pin" class="menu-item"><i data-lucide="pin" class="h-4 w-4"></i><span>Pin to top</span></button>
<button data-action="rename" class="menu-item"><i data-lucide="edit-3" class="h-4 w-4"></i><span>Rename</span></button>
<button data-action="pfp" class="menu-item"><i data-lucide="image" class="h-4 w-4"></i><span>Change pfp</span></button>
<button data-action="export" class="menu-item"><i data-lucide="download" class="h-4 w-4"></i><span>Export sune (.sune)</span></button>
</div>
<div id="suneModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div>
<div class="absolute inset-x-0 top-12 mx-auto w-full max-w-md px-4">
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center gap-2"><input id="suneURL" type="text" placeholder="" class="flex-1 min-w-0 h-10 rounded-xl border-0 bg-gray-50 px-3 text-gray-400 placeholder:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-200 focus:bg-white text-xs font-mono focus:text-black"/><button id="syncSune" class="p-1.5 rounded hover:bg-gray-100" aria-label="Refresh"><i data-lucide="refresh-cw" class="h-5 w-5"></i></button></div>
<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="openai/gpt-5"/><p class="mt-1 text-xs text-gray-500">Optional: prefix with or: or oai:</p></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="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select><p class="mt-1 text-xs text-gray-500">If supported by the model.</p></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.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Variety. Lower = predictable.</p></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"/><p class="mt-1 text-xs text-gray-500">Nucleus sampling.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Top K</label><input id="set_top_k" type="number" min="0" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0"/><p class="mt-1 text-xs text-gray-500">Token shortlist size.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Frequency Penalty <span class="text-gray-400">(-22)</span></label><input id="set_frequency_penalty" type="number" min="-2" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Discourage repeats by count.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Presence Penalty <span class="text-gray-400">(-22)</span></label><input id="set_presence_penalty" type="number" min="-2" max="2" step="0.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/><p class="mt-1 text-xs text-gray-500">Discourage seen tokens.</p></div>
<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.1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/><p class="mt-1 text-xs text-gray-500">Reduce verbatim echoes.</p></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"/><p class="mt-1 text-xs text-gray-500">Minimum token prob vs best.</p></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"/><p class="mt-1 text-xs text-gray-500">Adaptive nucleus filter.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Max Tokens</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=""/><p class="mt-1 text-xs text-gray-500">Leave blank to omit.</p></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><p class="mt-1 text-xs text-gray-500">OpenAI param.</p></div>
</div>
</div>
<div id="panelPrompt" class="p-4 space-y-4 hidden">
<div><label class="block text-gray-700 font-medium mb-1">System Prompt</label><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 class="mt-2 flex gap-2"><button type="button" id="copySystemPrompt" class="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteSystemPrompt" class="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200">Paste</button></div><p class="mt-1 text-xs text-gray-500">Saved per sune.</p></div>
</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>
<div class="pt-0">
<pre id="htmlEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5" contenteditable="plaintext-only" spellcheck="false"></pre>
<pre id="extensionHtmlEditor" class="w-full h-[50vh] p-3 rounded-xl border border-gray-300 bg-white overflow-auto font-mono text-[12px] leading-5 hidden" contenteditable="plaintext-only" spellcheck="false"></pre>
</div>
<div class="mt-2 flex gap-2"><button type="button" id="copyHTML" class="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200">Copy</button><button type="button" id="pasteHTML" class="px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200">Paste</button></div>
<p class="mt-1 text-xs text-gray-500">Scripts also run. extension.html runs after index.html.</p>
</div>
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
<button type="button" id="deleteSuneBtn" class="inline-flex items-center gap-2 px-3 py-2 rounded-xl border border-red-200 text-red-700 hover:bg-red-50"><svg viewBox="0 0 24 24" class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6v12a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2V6m-9 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg><span>Delete sune</span></button> <div class="flex items-center justify-end gap-2"><button type="button" id="cancelSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
</div>
</form>
</div>
</div>
</div>
<div id="accountSettingsModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div>
<div class="absolute inset-x-0 top-16 mx-auto w-full max-w-md px-4">
<div class="rounded-2xl bg-white shadow-xl border border-gray-200 overflow-hidden">
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Account Settings</span><button id="closeAccountSettings" class="p-1 rounded hover:bg-gray-100" aria-label="Close"><svg viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg></button></div>
<form id="accountSettingsForm" class="text-sm">
<div class="border-b flex text-xs font-medium"><button type="button" id="accountTabGeneral" class="flex-1 py-2 px-3 text-center border-b-2 border-black">General</button><button type="button" id="accountTabAPI" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">API</button></div>
<div id="accountPanelGeneral" class="p-4 space-y-4">
<div><label class="block text-gray-700 font-medium mb-1">Provider</label><select id="set_provider" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="openrouter">OpenRouter</option><option value="openai">OpenAI</option></select><p class="mt-1 text-xs text-gray-500">Or you can just prefix names with or: or oai: to override.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it" value="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
</div>
<div id="accountPanelAPI" class="p-4 space-y-4 hidden">
<div><label class="block text-gray-700 font-medium mb-1">OpenRouter API Key</label><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-or-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">OpenAI API Key</label><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="sk-..."/><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Github Token</label><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="ghp_... (optional)"/><p class="mt-1 text-xs text-gray-500">Optional. For future use.</p></div>
</div>
<div class="flex items-center justify-between gap-2 px-4 py-3 border-t">
<div class="flex items-center gap-2"><button type="button" id="importAccountSettings" class="text-xs px-2.5 py-1.5 rounded-lg border bg-white hover:bg-gray-50">Import</button><button type="button" id="exportAccountSettings" class="text-xs px-2.5 py-1.5 rounded-lg border bg-white hover:bg-gray-50">Export</button></div>
<div class="flex items-center justify-end gap-2"><button type="button" id="cancelAccountSettings" class="px-3 py-2 rounded-xl border bg-white hover:bg-gray-50">Cancel</button><button type="submit" class="px-3 py-2 rounded-xl bg-black text-white hover:bg-black/90">Save</button></div>
</div>
</form>
</div>
</div>
</div>
<input id="importAccountSettingsInput" type="file" class="hidden" accept="application/json,.json">
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.1/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"></script>
<script>
window.doCreateTitle=!0;
const DEFAULT_MODEL='openai/gpt-5',DEFAULT_API_KEY='',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_presence_penalty','set_repetition_penalty','set_min_p','set_top_a','set_max_tokens','set_verbosity','set_reasoning_effort','set_system_prompt','deleteSuneBtn','sidebarLeft','sidebarOverlayLeft','sidebarBtnLeft','suneList','newSuneBtn','userMenuBtn','userMenu','accountSettingsOption','sunesImportOption','sunesExportOption','threadsImportOption','threadsExportOption','importInput','sidebarBtnRight','sidebarRight','sidebarOverlayRight','threadList','closeThreads','threadPopover','sunePopover','footer','attachBtn','attachBadge','fileInput','htmlEditor','extensionHtmlEditor','htmlTab_index','htmlTab_extension','suneHtml','accountSettingsModal','accountSettingsForm','closeAccountSettings','cancelAccountSettings','set_master_prompt','set_provider','set_api_key_or','set_api_key_oai','set_title_model','copySystemPrompt','pasteSystemPrompt','copyHTML','pasteHTML','accountTabGeneral','accountTabAPI','accountPanelGeneral','accountPanelAPI','set_gh_token','importAccountSettings','exportAccountSettings','importAccountSettingsInput'].map(id=>[id,document.getElementById(id)])),icons=()=>window.lucide?.createIcons(),haptic=()=>/android/i.test(navigator.userAgent)&&navigator.vibrate?.(1),clamp=(v,m,x)=>Math.max(m,Math.min(x,v)),num=(v,d)=>v==null||v===''||isNaN(+v)?d:+v,int=(v,d)=>v==null||v===''||isNaN(parseInt(v))?d:parseInt(v),gid=()=>Math.random().toString(36).slice(2,9),esc=s=>String(s).replace(/[&<>'"`]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;","`":"&#96;"}[c])),sid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,6),fmtSize=b=>{const u=['B','KB','MB','GB','TB'];let i=0;for(x=b;x>=1024&&i<u.length-1;i++)x/=1024;return(x>=10?Math.round(x):Math.round(x*10)/10)+' '+u[i]},asDataURL=f=>new Promise(r=>{const fr=new FileReader;fr.onload=()=>r(String(fr.result||'')),fr.readAsDataURL(f)}),b64=x=>x.split(',')[1]||'';
const globalStore={get provider(){return localStorage.getItem('provider')||'openrouter'},set provider(v){localStorage.setItem('provider',v==='openai'?'openai':'openrouter')},get apiKeyOR(){return localStorage.getItem('openrouter_api_key')||DEFAULT_API_KEY||''},set apiKeyOR(v){localStorage.setItem('openrouter_api_key',v||'')},get apiKeyOAI(){return localStorage.getItem('openai_api_key')||''},set apiKeyOAI(v){localStorage.setItem('openai_api_key',v||'')},get masterPrompt(){return localStorage.getItem('master_prompt')||''},set masterPrompt(v){localStorage.setItem('master_prompt',v||'')},get titleModel(){return localStorage.getItem('title_model')??'or:meta-llama/llama-3.2-3b-instruct'},set titleModel(v){localStorage.setItem('title_model',v||'')},get ghToken(){return localStorage.getItem('gh_token')||''},set ghToken(v){localStorage.setItem('gh_token',v||'')}};
const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(l){localStorage.setItem(this.key,JSON.stringify(l||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}};
const defaultSettings={model:DEFAULT_MODEL,temperature:1,top_p:.96,top_k:0,frequency_penalty:0,presence_penalty:0,repetition_penalty:1,min_p:0,top_a:0,max_tokens:0,verbosity:'',reasoning_effort:'default',system_prompt:'',html:'',extension_html:''};
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:{...defaultSettings,...p.settings||{}}});
let sunes=(su.load()||[]).map(makeSune);
const SUNE=window.SUNE=new Proxy({get list(){return sunes},get id(){return su.getActiveId()},get active(){return sunes.find(a=>a.id===su.getActiveId())||sunes[0]},get:id=>sunes.find(s=>s.id===id),setActive:id=>su.setActiveId(id||''),create(p={}){const s=makeSune(p);sunes.unshift(s);su.save(sunes);return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);if(sunes.length===0){const def=this.create({name:'Default'});this.setActive(def.id)}else if(curId===id)this.setActive(sunes[0].id)},save:()=>su.save(sunes)},{get(t,p){if(p==='fetchDotSune')return async g=>{if(t.get('e1yibwd'))return;try{const[a,b]=g.split('@'),[c,d]=a.split('/'),[e,...f]=b.split('/'),u=`https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join('/')}`,j=await(await fetch(u)).json(),l=Array.isArray(j)?j:(j?.sunes||[]),s=l.find(i=>i.id==='e1yibwd');if(s&&!t.get(s.id)){sunes.unshift(makeSune(s));t.save()}}catch{}};if(p==='getThread')return id=>threads.find(t=>t.id===id)||null;if(p==='setThreadTitle')return async(id,title)=>{const th=threads.find(t=>t.id===id);if(!th||!title)return;th.title=titleFrom(title),th.updatedAt=Date.now(),await tsave(threads),await renderThreads()};if(p==='attach')return async(files,opts={})=>{const arr=[],o=opts===!0?{toAPI:!0,tree:!0}:opts||{};for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser(clean[0]?.name||'(attachments)');const meta=activeMeta(),toAPI=o.toAPI??o.toapi??!0,tree=o.tree??!0;if(toAPI)addMessage({role:'assistant',content:clean.map(a=>a.part),...meta});if(tree)addAttachmentTree('assistant',clean);await persistThread()};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 persistThread()};if(p in t)return t[p];if(p==='provider')return globalStore.provider;if(p==='apiKey')return globalStore.provider==='openai'?globalStore.apiKeyOAI:globalStore.apiKeyOR;if(p==='apiKeyOR')return globalStore.apiKeyOR;if(p==='apiKeyOAI')return globalStore.apiKeyOAI;if(p==='masterPrompt')return globalStore.masterPrompt;if(p==='titleModel')return globalStore.titleModel;const a=t.active;if(a){if(p in a.settings)return a.settings[p];if(p in a)return a[p]}},set(t,p,v){if(p==='provider'){globalStore.provider=v;return!0}if(p==='apiKey'){if(globalStore.provider==='openai')globalStore.apiKeyOAI=v;else globalStore.apiKeyOR=v;return!0}if(p==='apiKeyOR'){globalStore.apiKeyOR=v;return!0}if(p==='apiKeyOAI'){globalStore.apiKeyOAI=v;return!0}if(p==='masterPrompt'){globalStore.masterPrompt=v;return!0}if(p==='titleModel'){globalStore.titleModel=v;return!0}const a=t.active;if(!a)return!1;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return!1;if(p==='name'||p==='avatar'||p==='url'||p==='pinned')sunes[i][p]=v;else if(p==='model')sunes[i].settings.model=v||DEFAULT_MODEL;else if(p==='system_prompt')sunes[i].settings.system_prompt=v||'';else sunes[i].settings[p]=v;sunes[i].updatedAt=Date.now(),su.save(sunes);return!0}});
if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)}
const state=window.state={messages:[],busy:!1,controller:null,currentThreadId:null,abortRequested:!1,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:!1}},getModelShort=m=>(m||SUNE.model||'').split('/').pop(),resolveSuneSrc=src=>{if(!src)return null;if(src.startsWith('gh://')){const[owner,repo,...filePathParts]=src.substring(5).split('/');if(filePathParts.length<1)return null;return`https://raw.githubusercontent.com/${owner}/${repo}/main/${filePathParts.join('/')}`}return src},processSuneIncludes=async(html,depth=0)=>{if(depth>5||!html)return html?'<!-- Sune include depth limit reached -->':'';const c=document.createElement('div');c.innerHTML=html;for(const n of[...c.querySelectorAll('sune')]){if(n.hasAttribute('src')){if(n.hasAttribute('private')&&depth>0){n.remove();continue}const s=n.getAttribute('src'),u=resolveSuneSrc(s);if(!u){n.replaceWith(document.createComment(` Invalid src: ${esc(s)} `));continue}try{const r=await fetch(u);if(!r.ok)throw new Error(`HTTP ${r.status}`);const d=await r.json(),o=Array.isArray(d)?d[0]:d,h=[o?.settings?.html||'',o?.settings?.extension_html||''].join('\n');n.replaceWith(document.createRange().createContextualFragment(await processSuneIncludes(h,depth+1)))}catch(e){n.replaceWith(document.createComment(` Fetch failed: ${esc(u)} `))}}else n.replaceWith(document.createRange().createContextualFragment(n.innerHTML))}return c.innerHTML};
const renderSuneHTML=async()=>{const baseHtml=[SUNE.html,SUNE.extension_html].filter(Boolean).map(x=>x.trim()).join('\n'),finalHtml=await processSuneIncludes(baseHtml),container=el.suneHtml;container.innerHTML='',container.classList.toggle('hidden',!finalHtml.trim());if(!finalHtml.trim())return;container.insertAdjacentHTML('afterbegin',finalHtml);const scripts=[...container.querySelectorAll('script')].filter(s=>!s.type||s.type==='text/javascript').map(s=>(s.remove(),s.textContent));if(scripts.length)setTimeout(()=>scripts.forEach(code=>{try{new Function(code)()}catch(e){console.error('Sune script error:',e)}}),0)},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"/>`:'✺',await renderSuneHTML(),icons()},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-6 w-6 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>`,renderSidebar=window.renderSidebar=()=>{el.suneList.innerHTML=[...SUNE.list].sort((a,b)=>b.pinned-a.pinned).map(suneRow).join(''),icons()};
const enhanceCodeBlocks=root=>{root.querySelectorAll('pre>code').forEach(code=>{if(code.textContent.length>200000)return;const pre=code.parentElement;pre.classList.add('relative','rounded-xl','border','border-gray-200');if(!pre.querySelector('.copy-btn')){const btn=document.createElement('button');btn.className='copy-btn',btn.textContent='Copy',btn.onclick=async e=>{e.stopPropagation();try{await navigator.clipboard.writeText(code.innerText),btn.textContent='Copied',setTimeout(()=>btn.textContent='Copy',1200)}catch{}};pre.appendChild(btn)}window.hljs&&code.textContent.length<100000&&hljs.highlightElement(code)})},md=window.markdownit({html:!1,linkify:!0,typographer:!0,breaks:!0}),getSuneLabel=m=>`${m?.sune_name||SUNE.name} · ${getModelShort(m?.model)}`;
const msgRow=m=>{const role=typeof m=='string'?m:m?.role||'assistant',meta=typeof m=='string'?{}:m||{},row=document.createElement('div');row.className='flex flex-col gap-2';const head=document.createElement('div');head.className='flex items-center gap-2 px-4';const avatar=document.createElement('div');if(role==='user')avatar.className='bg-gray-900 text-white msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center',avatar.textContent='🧑';else if(meta?.avatar){avatar.className='msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden';const img=document.createElement('img');img.src=meta.avatar,img.className='h-full w-full object-cover',avatar.appendChild(img)}else avatar.className='bg-gray-200 text-gray-900 msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center',avatar.textContent='✺';const name=document.createElement('div');name.className='text-xs font-medium text-gray-500',name.textContent=role==='user'?'You':getSuneLabel(meta);const deleteBtn=document.createElement('button');deleteBtn.className='ml-auto p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-red-500',deleteBtn.title='Delete message',deleteBtn.innerHTML='<i data-lucide="eraser" class="h-4 w-4"></i>',deleteBtn.onclick=async e=>{e.stopPropagation();if(confirm('Delete message?'))state.messages=state.messages.filter(msg=>msg.id!==m.id),row.remove(),await persistThread()};const copyBtn=document.createElement('button');copyBtn.className='p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-gray-600',copyBtn.title='Copy message',copyBtn.innerHTML='<i data-lucide="copy" class="h-4 w-4"></i>',copyBtn.onclick=async function(e){e.stopPropagation();const b=this.parentElement.nextElementSibling;if(b)try{await navigator.clipboard.writeText(b.innerText),this.innerHTML='<i data-lucide="check" class="h-4 w-4 text-green-500"></i>',icons(),setTimeout(()=>{this.innerHTML='<i data-lucide="copy" class="h-4 w-4"></i>',icons()},1200)}catch{}};head.append(avatar,name,deleteBtn,copyBtn);const bubble=document.createElement('div');bubble.className=(role==='user'?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full',row.append(head,bubble),el.messages.appendChild(row),queueMicrotask(()=>{el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'}),icons()});return bubble};
const renderMarkdown=window.renderMarkdown=(node,text,opt={highlight:!0})=>{node.innerHTML=md.render(text),enhanceCodeBlocks(node,opt.highlight)},partsToText=parts=>Array.isArray(parts)?parts.map(p=>p?.type==='text'?p.text:(p?.type==='image_url'?`![](${p.image_url?.url||''})`:(p?.type==='file'?`[${p.file?.filename||'file'}]`:(p?.type==='input_audio'?`(audio:${p.input_audio?.format||''})`:'')))).join('\n'):String(parts||''),addMessage=window.addMessage=(m,track=!0)=>{m.id=m.id||gid();if(!Array.isArray(m.content)&&m.content!=null)m.content=[{type:'text',text:String(m.content)}];const bubble=msgRow(m);bubble.dataset.mid=m.id,renderMarkdown(bubble,partsToText(m.content));if(track)state.messages.push(m);return bubble},addSuneBubbleStreaming=meta=>msgRow({role:'assistant',...meta}),clearChat=()=>{state.messages=[],el.messages.innerHTML='',state.attachments=[],updateAttachBadge(),el.fileInput.value=''};
const payloadWithSampling=b=>{const o={...b};o.temperature=SUNE.temperature,o.top_p=SUNE.top_p,o.top_k=SUNE.top_k,o.frequency_penalty=SUNE.frequency_penalty,o.presence_penalty=SUNE.presence_penalty,o.repetition_penalty=SUNE.repetition_penalty,o.min_p=SUNE.min_p,o.top_a=SUNE.top_a;const mt=Math.max(0,int(SUNE.max_tokens||0,0));if(mt)o.max_tokens=mt;return o};
const 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=!0,state.controller?.abort?.()}},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},localDemoReply=()=>'Tip: open the sidebar → Account & Backup to set your API key.';
let threads=[];const titleFrom=t=>(t||'').replace(/\s+/g,' ').trim().slice(0,60)||'Untitled',TKEY='threads_v1',tload=()=>localforage.getItem(TKEY).then(v=>Array.isArray(v)?v:[]),tsave=v=>localforage.setItem(TKEY,v),cacheStore=localforage.createInstance({name:'master_cache'});
const ensureThreadOnFirstUser=async text=>{let needNew=!state.currentThreadId;if(state.messages.length===0)state.currentThreadId=null;if(state.currentThreadId&&!threads.some(x=>x.id===state.currentThreadId))needNew=!0;if(!needNew)return;const id=gid(),now=Date.now(),th={id,title:titleFrom(text),pinned:!1,updatedAt:now,messages:[]};state.currentThreadId=id,threads.unshift(th),await tsave(threads),await renderThreads(),document.dispatchEvent(new CustomEvent('sune:new-thread',{detail:{threadId:id}}))},persistThread=async(full=!0)=>{if(!state.currentThreadId)return;let th=threads.find(x=>x.id===state.currentThreadId);if(!th)return;th.messages=[...state.messages];if(full){th.updatedAt=Date.now(),th.title=titleFrom(partsToText(th.messages.find(m=>m.role==='user')?.content)||th.title)}await tsave(threads),full&&await renderThreads()},threadRow=t=>`<div class="relative flex items-center gap-2 px-3 py-2 ${t.pinned?'bg-yellow-50':''}"><button data-open-thread="${t.id}" class="flex-1 text-left truncate">${t.pinned?'📌 ':''}${esc(t.title)}</button><button data-thread-menu="${t.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>`,renderThreads=async()=>{el.threadList.innerHTML=[...threads].sort((a,b)=>(b.pinned-a.pinned)||(b.updatedAt-a.updatedAt)).map(threadRow).join(''),icons()};
let menuThreadId=null;const hideThreadPopover=()=>{el.threadPopover.classList.add('hidden'),menuThreadId=null},showThreadPopover=(btn,id)=>{menuThreadId=id;const r=btn.getBoundingClientRect();el.threadPopover.style.top=(r.bottom+4)+'px',el.threadPopover.style.left=Math.min(window.innerWidth-220,r.right-200)+'px',el.threadPopover.classList.remove('hidden'),icons()};
let menuSuneId=null;const hideSunePopover=()=>{el.sunePopover.classList.add('hidden'),menuSuneId=null},showSunePopover=(btn,id)=>{menuSuneId=id;const r=btn.getBoundingClientRect();el.sunePopover.style.top=(r.bottom+4)+'px',el.sunePopover.style.left=Math.min(window.innerWidth-220,r.right-200)+'px',el.sunePopover.classList.remove('hidden'),icons()};
el.threadList.addEventListener('click',async e=>{const openBtn=e.target.closest('[data-open-thread]'),menuBtn=e.target.closest('[data-thread-menu]');if(openBtn){const id=openBtn.dataset.openThread;if(id!==state.currentThreadId&&state.busy)state.controller?.disconnect?.(),setBtnSend(),state.busy=!1,state.controller=null;const th=threads.find(t=>t.id===id);if(!th)return;if(id===state.currentThreadId)return el.sidebarRight.classList.add('translate-x-full'),el.sidebarOverlayRight.classList.add('hidden'),hideThreadPopover();state.currentThreadId=id,clearChat(),state.messages=th.messages?[...th.messages]:[],state.messages.forEach(m=>{const b=msgRow(m);b.dataset.mid=m.id||'',renderMarkdown(b,partsToText(m.content))}),await renderSuneHTML(),syncWhileBusy(),queueMicrotask(()=>el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'})),el.sidebarRight.classList.add('translate-x-full'),el.sidebarOverlayRight.classList.add('hidden'),hideThreadPopover()}else if(menuBtn)e.stopPropagation(),showThreadPopover(menuBtn,menuBtn.dataset.threadMenu)});
el.threadPopover.addEventListener('click',async e=>{const act=e.target.closest('[data-action]')?.dataset.action;if(!act||!menuThreadId)return;const th=threads.find(t=>t.id===menuThreadId);if(!th)return;if(act==='pin')th.pinned=!th.pinned;else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null)th.title=titleFrom(nv),th.updatedAt=Date.now()}else if(act==='delete'){if(confirm('Delete this chat?'))threads=threads.filter(x=>x.id!==th.id),state.currentThreadId===th.id&&(state.currentThreadId=null,clearChat())}else if(act==='count_tokens'){const tokens=Math.max(0,Math.ceil((th.messages||[]).reduce((sum,m)=>sum+(m&&m.role&&m.role!=='system'?String(partsToText(m.content||'')||'').length:0),0)/4)),k=tokens>=1e3?Math.round(tokens/1e3)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}hideThreadPopover(),await tsave(threads),renderThreads()});
el.suneList.addEventListener('click',async e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn)return e.stopPropagation(),showSunePopover(menuBtn,menuBtn.dataset.suneMenu);const btn=e.target.closest('[data-sune-id]');if(btn){const id=btn.dataset.suneId;if(id){if(state.busy)state.controller?.disconnect?.(),setBtnSend(),state.busy=!1,state.controller=null;SUNE.setActive(id),renderSidebar(),await reflectActiveSune(),state.currentThreadId=null,clearChat(),el.sidebarLeft.classList.add('-translate-x-full'),el.sidebarOverlayLeft.classList.add('hidden')}}});
el.sunePopover.addEventListener('click',async e=>{const act=e.target.closest('[data-action]')?.dataset.action;if(!act||!menuSuneId)return;const s=SUNE.get(menuSuneId);if(!s)return;const updateAndRender=async()=>{s.updatedAt=Date.now(),SUNE.save(),renderSidebar(),await reflectActiveSune()};if(act==='pin')s.pinned=!s.pinned,await updateAndRender();else if(act==='rename'){const n=prompt('Rename sune to:',s.name);if(n!=null)s.name=n.trim(),await updateAndRender()}else if(act==='pfp'){const i=document.createElement('input');i.type='file',i.accept='image/*',i.onchange=()=>{const f=i.files?.[0];if(f){const img=new Image;img.onload=async()=>{const c=document.createElement('canvas'),ctx=c.getContext('2d'),D=144;let{width:w,height:h}=img;Math.max(w,h)>D&&(w>h?(h=D*h/w,w=D):(w=D*w/h,h=D)),c.width=w,c.height=h,ctx.drawImage(img,0,0,w,h),s.avatar=c.toDataURL('image/webp',.84),await updateAndRender(),URL.revokeObjectURL(img.src)},img.src=URL.createObjectURL(f)}},i.click()}else if(act==='export')dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${ts()}.sune`,[s]);hideSunePopover()});
const updateAttachBadge=()=>{const n=state.attachments.length;el.attachBadge.textContent=String(n),el.attachBadge.classList.toggle('hidden',n===0)},toAttach=async file=>{if(!file)return null;const pick=(name,bytes,mime,data,mode,part)=>({name,bytes,mime,data,mode,part});if(file instanceof File){const name=file.name||'file',mime=(file.type||'application/octet-stream').toLowerCase(),bytes=file.size||0,data=await asDataURL(file);if(/^image\//.test(mime)||/\.(png|jpe?g|webp|gif)$/i.test(name))return pick(name,bytes,mime,data,'dataURL',{type:'image_url',image_url:{url:data}});const bin=b64(data);if(mime==='application/pdf'||/\.pdf$/i.test(name))return pick(name.endsWith('.pdf')?name:name+'.pdf',bytes,'application/pdf',bin,'base64',{type:'file',file:{filename:name,file_data:bin}});if(/^audio\//.test(mime)||/\.(wav|mp3)$/i.test(name)){const fmt=/mp3/.test(mime)||/\.mp3$/i.test(name)?'mp3':'wav';return pick(name,bytes,mime,bin,'base64',{type:'input_audio',input_audio:{data:bin,format:fmt}})}return pick(name,bytes,mime,bin,'base64',{type:'file',file:{filename:name,file_data:bin}})}if(file&&file.name==null&&file.data){const name=file.name||'file',mime=(file.mime||'application/octet-stream').toLowerCase(),bytes=file.size||0;if(/^image\//.test(mime))return pick(name,bytes,mime,`data:${mime};base64,${file.data}`,'dataURL',{type:'image_url',image_url:{url:`data:${mime};base64,${file.data}`}});const bin=file.data;if(mime==='application/pdf')return pick(name,bytes,mime,bin,'base64',{type:'file',file:{filename:name,file_data:bin}});if(/^audio\//.test(mime))return pick(name,bytes,mime,bin,'base64',{type:'input_audio',input_audio:{data:bin,format:/mp3/.test(mime)?'mp3':'wav'}});return pick(name,bytes,mime,bin,'base64',{type:'file',file:{filename:name,file_data:bin}})}return null},attachmentsText=(id,arr)=>`**Attachments**\n${arr.map((a,i)=>`- [${esc(a.name)}${fmtSize(a.bytes)}](#dl-${id}-${i})`).join('\n')}`,addAttachmentTree=(role,arr)=>{if(!arr?.length)return;const id=gid(),text=attachmentsText(id,arr),meta={role,content:[{type:'text',text}],id,kind:'attachments',attachmentsMeta:arr.map(a=>({name:a.name,bytes:a.bytes,mime:a.mime,mode:a.mode,data:a.data}))},b=addMessage(meta,!0);b.dataset.mid=id};
el.attachBtn.addEventListener('click',()=>{if(!state.busy)state.attachments.length?(state.attachments=[],updateAttachBadge(),el.fileInput.value=''):el.fileInput.click()});
el.fileInput.addEventListener('change',async()=>{const files=el.fileInput.files||[];if(files.length)await Promise.all([...files].map(async f=>{const at=await toAttach(f).catch(()=>null);if(at)state.attachments.push(at)})),updateAttachBadge()});
el.messages.addEventListener('click',async e=>{const a=e.target.closest('a[href^="#dl-"]');if(!a)return;e.preventDefault();const m=a.getAttribute('href').match(/^#dl-([^-]+)-(\d+)$/);if(!m)return;const msg=state.messages.find(x=>x.id===m[1]),meta=msg?.attachmentsMeta?.[+m[2]];if(!meta)return;const blob=meta.mode==='dataURL'?await(await fetch(meta.data)).blob():new Blob([Uint8Array.from(atob(meta.data),c=>c.charCodeAt(0))],{type:meta.mime||'application/octet-stream'}),url=URL.createObjectURL(blob),dl=document.createElement('a');dl.href=url,dl.download=meta.name||'download',document.body.appendChild(dl),dl.click(),dl.remove(),URL.revokeObjectURL(url)});
el.composer.addEventListener('submit',async e=>{e.preventDefault();if(state.busy)return;const text=el.input.value.trim();if(!text&&!state.attachments.length)return;state.messages.length===0&&(state.currentThreadId=null),await ensureThreadOnFirstUser(text||'(attachments)'),el.input.value='';const parts=[];if(text)parts.push({type:'text',text});state.attachments.forEach(a=>parts.push(a.part)),addMessage({role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]}),state.attachments.length&&addAttachmentTree('user',state.attachments),state.busy=!0,setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},suneBubble=addSuneBubbleStreaming(suneMeta),streamId=sid();suneBubble.dataset.mid=streamId;const assistantMsg={id:streamId,role:'assistant',content:[{type:'text',text:''}],...suneMeta};state.messages.push(assistantMsg),persistThread(!1),state.stream={rid:streamId,bubble:suneBubble,meta:suneMeta,text:'',done:!1};let buf='',completed=!1;const onDelta=(delta,done)=>{buf+=delta,state.stream.text=buf,renderMarkdown(suneBubble,buf,{highlight:!1}),assistantMsg.content[0].text=buf;if(done&&!completed)completed=!0,setBtnSend(),state.busy=!1,enhanceCodeBlocks(suneBubble),persistThread(),state.stream={rid:null,bubble:null,meta:null,text:'',done:!1};else if(!done)persistThread(!1)};await askOpenRouterStreaming(onDelta,streamId),state.attachments=[],updateAttachBadge()});
let jars={html:null,extension:null};const ensureJars=async()=>{if(jars.html&&jars.extension)return jars;const{CodeJar:C}=await import('https://medv.io/codejar/codejar.js'),hl=e=>e.innerHTML=hljs.highlight(e.textContent,{language:'xml'}).value;return jars.html=C(el.htmlEditor,hl,{tab:' '}),jars.extension=C(el.extensionHtmlEditor,hl,{tab:' '}),jars};
let openedHTML=!1;const openSettings=()=>{const a=SUNE.active,s=a.settings;openedHTML=!1,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_presence_penalty.value=s.presence_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_verbosity.value=s.verbosity||'',el.set_reasoning_effort.value=s.reasoning_effort||'default',el.set_system_prompt.value=s.system_prompt,showTab('Model'),el.suneModal.classList.remove('hidden')},closeSettings=()=>el.suneModal.classList.add('hidden'),tabs={Model:['tabModel','panelModel'],Prompt:['tabPrompt','panelPrompt'],Script:['tabScript','panelScript']},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=!0,showHtmlTab('index'),ensureJars().then(({html,extension})=>{const s=SUNE.settings;html.updateCode(s.html||''),extension.updateCode(s.extension_html||'')})}};
el.suneBtnTop.addEventListener('click',openSettings),el.cancelSettings.addEventListener('click',closeSettings),el.suneModal.addEventListener('click',e=>(e.target===el.suneModal||e.target.classList.contains('bg-black/30'))&&closeSettings()),el.tabModel.addEventListener('click',()=>showTab('Model')),el.tabPrompt.addEventListener('click',()=>showTab('Prompt')),el.tabScript.addEventListener('click',()=>showTab('Script'));
el.settingsForm.addEventListener('submit',async e=>{e.preventDefault(),SUNE.url=(el.suneURL.value||'').trim(),SUNE.model=(el.set_model.value||DEFAULT_MODEL).trim(),SUNE.temperature=clamp(num(el.set_temperature.value,1),0,2),SUNE.top_p=clamp(num(el.set_top_p.value,1),0,1),SUNE.top_k=Math.max(0,int(el.set_top_k.value,0)),SUNE.frequency_penalty=clamp(num(el.set_frequency_penalty.value,0),-2,2),SUNE.presence_penalty=clamp(num(el.set_presence_penalty.value,0),-2,2),SUNE.repetition_penalty=clamp(num(el.set_repetition_penalty.value,1),0,2),SUNE.min_p=clamp(num(el.set_min_p.value,0),0,1),SUNE.top_a=clamp(num(el.set_top_a.value,0),0,1),SUNE.max_tokens=Math.max(0,int(el.set_max_tokens.value,0)),SUNE.verbosity=el.set_verbosity.value||'',SUNE.reasoning_effort=el.set_reasoning_effort.value||'default',SUNE.system_prompt=el.set_system_prompt.value.trim(),openedHTML&&(SUNE.html=el.htmlEditor.textContent,SUNE.extension_html=el.extensionHtmlEditor.textContent),closeSettings(),await reflectActiveSune()});
el.deleteSuneBtn.addEventListener('click',async()=>{const activeId=SUNE.id,name=SUNE.name||'this sune';if(confirm(`Delete "${name}"?`))SUNE.delete(activeId),renderSidebar(),await reflectActiveSune(),state.currentThreadId=null,clearChat(),closeSettings()}),el.newSuneBtn.addEventListener('click',async()=>{const name=prompt('Name your sune:');if(name){const sune=SUNE.create({name:name.trim()});SUNE.setActive(sune.id),renderSidebar(),await reflectActiveSune(),state.currentThreadId=null,clearChat(),el.sidebarLeft.classList.add('-translate-x-full'),el.sidebarOverlayLeft.classList.add('hidden')}});
const dl=(name,obj)=>{const blob=new Blob([JSON.stringify(obj,null,2)],{type:name.endsWith('.sune')?'application/octet-stream':'application/json'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url,a.download=name,document.body.appendChild(a),a.click(),a.remove(),URL.revokeObjectURL(url)},ts=()=>{const d=new Date,p=n=>String(n).padStart(2,'0');return`${d.getFullYear()}${p(d.getMonth()+1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`};
let importMode=null;el.sunesExportOption.onclick=()=>dl(`sunes-${ts()}.sune`,{version:1,sunes:SUNE.list,activeId:SUNE.id}),el.sunesImportOption.onclick=()=>(importMode='sunes',el.importInput.value='',el.importInput.click()),el.threadsExportOption.onclick=()=>dl(`threads-${ts()}.json`,{version:1,threads}),el.threadsImportOption.onclick=()=>(importMode='threads',el.importInput.value='',el.importInput.click());
el.importInput.addEventListener('change',async()=>{const file=el.importInput.files?.[0];if(!file)return;try{const data=JSON.parse(await file.text());if(importMode==='sunes'){const list=(Array.isArray(data)?data:data.sunes)||[];if(!list.length)throw new Error('No sunes');const map={},incoming=list.map(a=>makeSune(a||{}));incoming.forEach(s=>{s.id=s.id||gid();const k=s.id,prev=map[k];map[k]=!prev||+s.updatedAt>+prev.updatedAt?s:prev});let added=0,updated=0;const idx=Object.fromEntries(sunes.map(s=>[s.id,s]));Object.values(map).forEach(s=>{const ex=idx[s.id];if(!ex)sunes.push(s),added++;else if(+s.updatedAt>+ex.updatedAt)Object.assign(ex,s),updated++;});SUNE.save(),data.activeId&&sunes.some(x=>x.id===data.activeId)&&SUNE.setActive(data.activeId),renderSidebar(),await reflectActiveSune(),state.currentThreadId=null,clearChat(),alert(`${added} new, ${updated} updated.`)}else if(importMode==='threads'){const arr=(Array.isArray(data)?data:data.threads)||[];if(!arr.length)throw new Error('No threads');const norm=t=>({id:t.id||gid(),title:titleFrom(t.title||t.messages?.find?.(m=>m.role==='user')?.content),pinned:!!t.pinned,updatedAt:t.updatedAt||Date.now(),messages:(t.messages||[]).filter(m=>m&&m.role&&m.content)}),best={};arr.forEach(t=>{const n=norm(t),k=n.id;(!best[k]||+n.updatedAt>+best[k].updatedAt)&&(best[k]=n)});let kept=0,skipped=0;const idx=Object.fromEntries(threads.map(t=>[t.id,t]));Object.values(best).forEach(th=>{const ex=idx[th.id];if(ex&&+ex.updatedAt>=+th.updatedAt)skipped++;else !ex?threads.push(th):Object.assign(ex,th),kept++;});await tsave(threads),await renderThreads(),alert(`${kept} imported, ${skipped} skipped (older).`)}el.userMenu.classList.add('hidden')}catch{alert('Import failed')}finally{importMode=null}});
const kbUpdate=()=>{const v=window.visualViewport,overlap=v?Math.max(0,window.innerHeight-(v.height+v.offsetTop)):0,fh=el.footer.getBoundingClientRect().height;document.documentElement.style.setProperty('--kb',overlap+'px'),document.documentElement.style.setProperty('--footer-h',fh+'px'),el.footer.style.transform=`translateY(${-overlap}px)`,el.chat.style.scrollPaddingBottom=`${fh+overlap+16}px`},kbBind=()=>{window.visualViewport&&['resize','scroll'].forEach(ev=>visualViewport.addEventListener(ev,kbUpdate,{passive:!0}));['resize','orientationchange'].forEach(ev=>window.addEventListener(ev,()=>setTimeout(kbUpdate,50),{passive:!0}));['focus','click'].forEach(ev=>el.input.addEventListener(ev,()=>{setTimeout(()=>{kbUpdate(),el.input.scrollIntoView({block:'nearest',behavior:'smooth'})},0)}))},activeMeta=()=>({sune_name:SUNE.name,model:SUNE.model,avatar:SUNE.avatar});
window.USER={log:async s=>{const t=String(s??'').trim();if(t)await ensureThreadOnFirstUser(t),addMessage({role:'user',content:[{type:'text',text:t}]}),await persistThread()},get PAT(){return globalStore.ghToken}};
const init=async()=>{await SUNE.fetchDotSune('sune-org/store@main/marketplace.sune'),threads=await tload(),await renderThreads(),renderSidebar(),await reflectActiveSune(),clearChat(),icons(),kbBind(),kbUpdate()};
window.addEventListener('resize',()=>{hideThreadPopover(),hideSunePopover()});
const htmlTabs={index:['htmlTab_index','htmlEditor'],extension:['htmlTab_extension','extensionHtmlEditor']},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.onclick=()=>showHtmlTab('index'),el.htmlTab_extension.onclick=()=>showHtmlTab('extension');
const HTTP_BASE='https://orp.awww.workers.dev/ws',buildBody=()=>{const msgs=[];if(SUNE.masterPrompt)msgs.push({role:'system',content:[{type:'text',text:SUNE.masterPrompt}]});if(SUNE.system_prompt)msgs.push({role:'system',content:[{type:'text',text:SUNE.system_prompt}]});msgs.push(...state.messages.filter(m=>m.role!=='system').map(m=>({role:m.role,content:m.content})));const b=payloadWithSampling({model:SUNE.model.replace(/^(or:|oai:)/,''),messages:msgs,stream:!0});if(SUNE.reasoning_effort&&SUNE.reasoning_effort!=='default')b.reasoning={effort:SUNE.reasoning_effort};if(SUNE.verbosity)b.verbosity=SUNE.verbosity;return b};
const askOpenRouterStreaming=async(onDelta,streamId)=>{const model=SUNE.model,provider=model.startsWith('oai:')?'openai':model.startsWith('or:')?'openrouter':SUNE.provider,apiKey=provider==='openai'?SUNE.apiKeyOAI:SUNE.apiKeyOR;if(!apiKey)return onDelta(localDemoReply(),!0),{ok:!0,rid:streamId||null};const r={rid:streamId||gid(),seq:-1,done:!1,signaled:!1,ws:null};await cacheStore.setItem(r.rid,'busy');const signal=t=>{if(!r.signaled)r.signaled=!0,onDelta(t||'',!0)};const ws=new WebSocket(`${HTTP_BASE.replace('https','wss')}?uid=${encodeURIComponent(r.rid)}`);r.ws=ws,ws.onopen=()=>ws.send(JSON.stringify({type:'begin',rid:r.rid,provider,apiKey,or_body:buildBody()})),ws.onmessage=e=>{let m;try{m=JSON.parse(e.data)}catch{return}if(m.type==='delta'&&typeof m.seq=='number'&&m.seq>r.seq)r.seq=m.seq,onDelta(m.text||'',!1);else if(m.type==='done'||m.type==='err')r.done=!0,cacheStore.setItem(r.rid,'done'),signal(m.type==='err'?'\n\n'+(m.message||'error'):''),ws.close()},ws.onclose=()=>{},ws.onerror=()=>{},state.controller={abort:()=>{r.done=!0,cacheStore.setItem(r.rid,'done');try{ws.readyState===1&&ws.send(JSON.stringify({type:'stop',rid:r.rid}))}catch{}signal('')},disconnect:()=>ws.close()};return{ok:!0,rid:r.rid}};
const accountTabs={General:['accountTabGeneral','accountPanelGeneral'],API:['accountTabAPI','accountPanelAPI']},showAccountTab=key=>{Object.entries(accountTabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key),el[pn].classList.toggle('hidden',k!==key)})},openAccountSettings=()=>{el.set_provider.value=SUNE.provider||'openrouter',el.set_api_key_or.value=SUNE.apiKeyOR||'',el.set_api_key_oai.value=SUNE.apiKeyOAI||'',el.set_master_prompt.value=SUNE.masterPrompt||'',el.set_title_model.value=SUNE.titleModel,el.set_gh_token.value=globalStore.ghToken||'',showAccountTab('General'),el.accountSettingsModal.classList.remove('hidden')},closeAccountSettings=()=>el.accountSettingsModal.classList.add('hidden');
el.accountSettingsOption.addEventListener('click',()=>{el.userMenu.classList.add('hidden'),openAccountSettings()}),el.closeAccountSettings.addEventListener('click',closeAccountSettings),el.cancelAccountSettings.addEventListener('click',closeAccountSettings),el.accountSettingsModal.addEventListener('click',e=>(e.target===el.accountSettingsModal||e.target.classList.contains('bg-black/30'))&&closeAccountSettings());
el.accountSettingsForm.addEventListener('submit',e=>{e.preventDefault(),SUNE.provider=el.set_provider.value||'openrouter',SUNE.apiKeyOR=String(el.set_api_key_or.value||'').trim(),SUNE.apiKeyOAI=String(el.set_api_key_oai.value||'').trim(),SUNE.masterPrompt=String(el.set_master_prompt.value||'').trim(),SUNE.titleModel=String(el.set_title_model.value||'').trim(),globalStore.ghToken=String(el.set_gh_token.value||'').trim(),closeAccountSettings()});
el.accountTabGeneral.onclick=()=>showAccountTab('General'),el.accountTabAPI.onclick=()=>showAccountTab('API'),el.exportAccountSettings.onclick=()=>dl(`sune-account-${ts()}.json`,{v:1,provider:globalStore.provider,apiKeyOR:globalStore.apiKeyOR,apiKeyOAI:globalStore.apiKeyOAI,masterPrompt:globalStore.masterPrompt,titleModel:globalStore.titleModel,ghToken:globalStore.ghToken}),el.importAccountSettings.onclick=()=>{el.importAccountSettingsInput.value='',el.importAccountSettingsInput.click()},el.importAccountSettingsInput.onchange=async e=>{const f=e.target.files?.[0];if(f)try{const d=JSON.parse(await f.text());if(d&&typeof d=='object')Object.entries({provider:'string',apiKeyOR:'string',apiKeyOAI:'string',masterPrompt:'string',titleModel:'string',ghToken:'string'}).forEach(([k,t])=>{if(typeof d[k]===t)k==='ghToken'?globalStore[k]=d[k]:SUNE[k]=d[k]}),openAccountSettings(),alert('Imported.')}catch{alert('Import failed')}};
const lastAssistantId=()=>{const a=[...el.messages.querySelectorAll('.msg-bubble')];for(let i=a.length-1;i>=0;i--){const b=a[i],h=b.previousElementSibling;if(h&&!/^\s*You\b/.test(h.textContent||''))return b.dataset.mid||null}return null},getBubbleById=id=>el.messages.querySelector(`.msg-bubble[data-mid="${CSS.escape(id)}"]`);
const syncActiveThread=async()=>{const id=lastAssistantId();if(!id||await cacheStore.getItem(id)==='done'){if(state.busy)setBtnSend(),state.busy=!1,state.controller=null;return!1}if(!state.busy)state.busy=!0,state.controller={abort:()=>{const ws=new WebSocket(HTTP_BASE.replace('https','wss'));ws.onopen=function(){this.send(JSON.stringify({type:'stop',rid:id})),this.close()}}},setBtnStop();const bubble=getBubbleById(id);if(!bubble)return!1;const prevText=bubble.textContent||'',j=await fetch(`${HTTP_BASE}?uid=${encodeURIComponent(id)}`).then(r=>r.ok?r.json():null).catch(()=>null);const finalise=(t,c)=>{renderMarkdown(bubble,t,{highlight:!1}),enhanceCodeBlocks(bubble),state.messages.find(x=>x.id===id).content=c,persistThread(),setBtnSend(),state.busy=!1,cacheStore.setItem(id,'done'),state.controller=null};if(!j||j.rid!==id){if(j?.error)finalise(prevText+'\n\n'+j.error,[{type:'text',text:prevText+'\n\n'+j.error}]);return!1}const text=j.text||'',isDone=j.error||j.done||j.phase==='done';if(text)renderMarkdown(bubble,text,{highlight:!1});if(isDone)return finalise(text||prevText,[{type:'text',text:text||prevText}]),!1;return await cacheStore.setItem(id,'busy'),!0};
let syncLoopRunning=!1;const syncWhileBusy=async()=>{if(syncLoopRunning||document.visibilityState==='hidden')return;syncLoopRunning=!0;try{while(await syncActiveThread())await new Promise(r=>setTimeout(r,1200))}finally{syncLoopRunning=!1}},onForeground=()=>{if(document.visibilityState==='visible')state.controller?.disconnect?.(),state.busy&&syncWhileBusy()};
document.addEventListener('visibilitychange',onForeground);
el.copySystemPrompt.onclick=async()=>await navigator.clipboard.writeText(el.set_system_prompt.value||'').catch(()=>{}),el.pasteSystemPrompt.onclick=async()=>el.set_system_prompt.value=await navigator.clipboard.readText().catch(()=>'');
const getActiveHtmlParts=()=>el.htmlEditor.classList.contains('hidden')?[el.extensionHtmlEditor,jars.extension]:[el.htmlEditor,jars.html];
el.copyHTML.onclick=async()=>await navigator.clipboard.writeText(getActiveHtmlParts()[0].textContent||'').catch(()=>{}),el.pasteHTML.onclick=async()=>{try{const t=await navigator.clipboard.readText(),[editor,jar]=getActiveHtmlParts();jar?.updateCode?jar.updateCode(t):editor&&(editor.textContent=t)}catch{}};
init();
</script>
</body>
</html>