Author SHA1 Message Date
multipleof4 c31d01520e Change Android package name to chat.sune.app 2026-10-01 21:37:07 +00:00
github-actions[bot] d3a206a70f This build was committed by a bot. 2026-10-01 19:44:25 +00:00
multipleof4andClaude Sonnet 5.5 3d308bdbaf Make web manifest URLs relative so sune.chat and sune.planetrenox.com both work
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:44:11 -07:00
multipleof4andClaude Sonnet 5.5 50219444f4 Use bare host for additionalTrustedOrigins
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:32:43 -07:00
multipleof4andClaude Sonnet 5.5 12cd60829f Point Gradle at the SDK Bubblewrap uses
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:27:57 -07:00
multipleof4andClaude Sonnet 5.5 ae05a6fd37 Add Android APK release workflow
Bubblewrap TWA config for chat.sune, plus a manually dispatched workflow
that builds, signs, verifies against assetlinks.json, attests provenance
and publishes the release for the tag given as input.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:25:57 -07:00
github-actions[bot] c321396ac4 This build was committed by a bot. 2026-10-01 18:11:39 +00:00
multipleof4andClaude Opus 5.5 be148beed9 Add chat.sune signing key to assetlinks
Keep the legacy com.planetrenox.sune entry so the old app stays
fullscreen, and add the new chat.sune package with its new key.
Ignore keystore files so signing keys can never be committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:11:19 -07:00
multipleof4andClaude Opus 5.5 3bca5eb74e Add third-party notices for bundled dist code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:52:57 +00:00
github-actions[bot] 72d36a63fe This build was committed by a bot. 2026-09-28 17:43:32 +00:00
multipleof4andClaude Opus 5.5 2cb952a7e3 Remove Cloudflare inference provider branches, keep key for custom sunes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:43:15 +00:00
github-actions[bot] 609f84017f This build was committed by a bot. 2026-09-28 17:41:08 +00:00
multipleof4andClaude Opus 5.5 b697398096 Drop unsupported cf: model prefix, fit account settings on small screens
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:40:14 +00:00
github-actions[bot] 15ee08a3f7 This build was committed by a bot. 2026-09-28 17:35:56 +00:00
multipleof4andClaude Opus 5.5 61d7fca337 Fit sune settings modal on small screens, trim Claude help note
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:35:38 +00:00
github-actions[bot] ed4999598b This build was committed by a bot. 2026-09-28 17:30:51 +00:00
multipleof4andClaude Opus 5.5 360e20bbad Add model name help popup to sune settings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:30:30 +00:00
github-actions[bot] 8d8dd84240 This build was committed by a bot. 2026-09-20 17:07:21 +00:00
multipleof4 35c2537f42 Increase max_tokens from 6 to 7 2026-09-20 10:07:08 -07:00
github-actions[bot] d3e56d2b88 This build was committed by a bot. 2026-09-18 18:52:27 +00:00
multipleof4 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
multipleof4 916f41606c Submit composer with Enter on desktop 2026-09-16 20:55:57 -07:00
multipleof4 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
multipleof4 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
multipleof4 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
multipleof4 599989fbed Shorten model name placeholder hint 2026-09-09 22:03:58 -07:00
multipleof4 27cbdea778 Support model name placeholder in system prompts 2026-09-09 21:59:49 -07:00
github-actions[bot] 4dd5916862 This build was committed by a bot. 2026-09-08 04:03:35 +00:00
multipleof4andgemini-3.8-flash 5d27b9904a Style: Adjust topbar padding to py-2
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 21:03:19 -07:00
github-actions[bot] 0bc73c7e80 This build was committed by a bot. 2026-09-08 04:02:49 +00:00
multipleof4andgemini-3.8-flash 22f578dbd1 Feat: Update topbar avatar image to h-10 w-10
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 21:02:33 -07:00
multipleof4andgemini-3.8-flash 5c9fe41f1c Feat: Set topbar Sune avatar to h-10 w-10
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 21:02:31 -07:00
github-actions[bot] c028b505c9 This build was committed by a bot. 2026-09-08 03:52:11 +00:00
multipleof4andgemini-3.8-flash 567a082fda Perf: Default avatar resize to 96px and 75q
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 20:51:52 -07:00
multipleof4andgemini-3.8-flash 1a134c82ca Feat: Update topbar avatar image to h-9 w-9
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 20:51:37 -07:00
multipleof4andgemini-3.8-flash c362744e39 Feat: Enlarge topbar Sune avatar to h-9 w-9
Co-authored-by: gemini-3.8-flash <noreply@google.com>
2026-09-07 20:51:35 -07:00
github-actions[bot] 3c2fd49227 This build was committed by a bot. 2026-09-08 03:18:00 +00:00
multipleof4 2e840b4b86 refactor(utils): update imgToWebp defaults to D=96 and q=75 2026-09-07 20:17:35 -07:00
github-actions[bot] 4b6ef90d3a This build was committed by a bot. 2026-09-06 04:03:51 +00:00
multipleof4 6c4376ca71 Set omitted reasoning_effort to empty string 2026-09-05 20:59:46 -07:00
github-actions[bot] dec4bd1fb3 This build was committed by a bot. 2026-09-06 01:23:14 +00:00
multipleof4 a4f04d3aac Replace verbosity setting with max tokens (output) 2026-09-05 18:20:23 -07:00
multipleof4 8b9adc8d64 Update readme.md 2026-09-05 14:34:40 -07:00
multipleof4 8128ce5c63 Merge pull request #23 from sune-org/upgrade_github_sync_logic
Upgrade GitHub sync logic
2026-09-04 11:56:45 -07:00
multipleof4 fa745efbab Merge pull request #22 from sune-org/upgrade_github_sync_logic
Upgrade GitHub sync logic
2026-09-04 09:53:30 -07:00
24 changed files with 588 additions and 122 deletions
+1 -1
View File
@@ -1 +1 @@
custom: https://paypal.me/planetrenox github: multipleof4
+141
View File
@@ -0,0 +1,141 @@
name: Release APK
run-name: Release APK ${{ inputs.version }}
on:
workflow_dispatch:
inputs:
version:
description: 'Release tag to create, e.g. v1.0.0'
required: true
type: string
concurrency: release-apk
permissions: {}
jobs:
apk:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write # create the tag + release
id-token: write # sign the build attestation
attestations: write
env:
TAG: ${{ inputs.version }}
steps:
- name: Validate version
run: |
[[ "$TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] || { echo "::error::version must look like v1.0.0"; exit 1; }
MA=${BASH_REMATCH[1]} MI=${BASH_REMATCH[2]} PA=${BASH_REMATCH[3]}
(( MI < 100 && PA < 100 )) || { echo "::error::minor and patch must be < 100"; exit 1; }
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
echo "CODE=$((MA*10000 + MI*100 + PA))" >> $GITHUB_ENV
echo "APK=sune-$TAG.apk" >> $GITHUB_ENV
echo "BT=$RUNNER_TEMP/sdk/build-tools/36.1.0" >> $GITHUB_ENV
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Fail if the release already exists
if: github.ref == 'refs/heads/master'
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::error::$TAG already exists"; exit 1
fi
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: 17
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
- name: Install Android SDK + Bubblewrap
run: |
SDK=$RUNNER_TEMP/sdk
mkdir -p "$SDK/bin" # bubblewrap only checks that this exists
SM=$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager
yes | "$SM" --sdk_root="$SDK" --licenses >/dev/null || true # yes exits on SIGPIPE
"$SM" --sdk_root="$SDK" "build-tools;36.1.0" "platforms;android-36" >/dev/null
echo "ANDROID_HOME=$SDK" >> $GITHUB_ENV # the runner's own ANDROID_SDK_ROOT would conflict
echo "ANDROID_SDK_ROOT=$SDK" >> $GITHUB_ENV
npm install --global --ignore-scripts @bubblewrap/cli@1.25.0
mkdir -p ~/.bubblewrap
jq -n --arg jdk "$JAVA_HOME" --arg sdk "$SDK" '{jdkPath:$jdk,androidSdkPath:$sdk}' > ~/.bubblewrap/config.json
- name: Build + sign
env:
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
BUBBLEWRAP_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASS }}
BUBBLEWRAP_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASS }}
run: |
B=$RUNNER_TEMP/build OUT=$RUNNER_TEMP/out
mkdir -p "$B" "$OUT"
echo "$ANDROID_KEYSTORE_B64" | base64 -d > "$B/sune.jks"
# pin the icon to this exact commit instead of whatever the site serves today
ICON="https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$GITHUB_SHA/public/appstore_content/%E2%9C%BA.png"
jq --arg v "$VERSION" --argjson c "$CODE" --arg a "$ANDROID_KEY_ALIAS" --arg i "$ICON" \
'.appVersion=$v | .appVersionCode=$c | .signingKey.alias=$a | .iconUrl=$i | .maskableIconUrl=$i' \
android/twa-manifest.json > "$B/twa-manifest.json"
cd "$B"
bubblewrap update --manifest ./twa-manifest.json --skipVersionUpgrade
bubblewrap build --manifest ./twa-manifest.json --skipPwaValidation
mv app-release-signed.apk "$OUT/$APK"
rm -f sune.jks
echo "OUT=$OUT" >> $GITHUB_ENV
- name: Verify the APK
run: |
cd "$OUT"
"$BT/apksigner" verify --verbose --print-certs "$APK" | tee verify.txt
grep -q "Number of signers: 1" verify.txt
GOT=$(sed -n 's/.*certificate SHA-256 digest: //p' verify.txt | sed -n 1p)
WANT=$(jq -r '.[].target | select(.package_name=="chat.sune.app") | .sha256_cert_fingerprints[]' "$GITHUB_WORKSPACE/public/.well-known/assetlinks.json" | tr -d ':' | tr A-F a-f)
[ "$GOT" = "$WANT" ] || { echo "::error::signing cert $GOT does not match assetlinks.json ($WANT)"; exit 1; }
"$BT/aapt2" dump badging "$APK" > full-badging.txt
grep -m1 '^package:' full-badging.txt | tee badging.txt
grep -q "name='chat.sune.app' versionCode='$CODE' versionName='$VERSION'" badging.txt
sha256sum "$APK" | tee "$APK.sha256"
echo "SIGNER=$GOT" >> $GITHUB_ENV
- name: Attest build provenance
if: github.ref == 'refs/heads/master'
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: ${{ runner.temp }}/out/sune-${{ inputs.version }}.apk
- name: Publish release
if: github.ref == 'refs/heads/master'
env:
GH_TOKEN: ${{ github.token }}
run: |
cd "$OUT"
SHA=$(cut -d' ' -f1 "$APK.sha256")
cat > notes.md <<EOF
Built and signed by GitHub Actions from commit \`$GITHUB_SHA\` ([workflow run]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)). Nobody handled the file in between.
Verify a download:
\`\`\`
gh attestation verify $APK --repo $GITHUB_REPOSITORY
\`\`\`
- Package: \`chat.sune.app\`
- SHA-256: \`$SHA\`
- Signing certificate SHA-256: \`$SIGNER\`
EOF
gh release create "$TAG" "$APK" "$APK.sha256" --repo "$GITHUB_REPOSITORY" --target "$GITHUB_SHA" --title "Sune $TAG" --notes-file notes.md
- name: Keep the APK as a workflow artifact (non-release runs)
if: github.ref != 'refs/heads/master'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.APK }}
path: ${{ runner.temp }}/out/*.apk
retention-days: 3
+5
View File
@@ -24,3 +24,8 @@ dist-ssr
package-lock.json package-lock.json
bun.lock bun.lock
# Signing keys (live in Infisical /sune, never commit)
*.jks
*.keystore
*.p12
+85
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.
+57
View File
@@ -0,0 +1,57 @@
# Third-Party Notices
Sune's own code is covered by [LICENSE](./LICENSE). This file covers third-party code that this repository redistributes in the committed build output (`dist/`).
## Bundled in `dist/`
| Component | Where | License | Copyright |
|---|---|---|---|
| [Workbox](https://github.com/GoogleChrome/workbox) | `dist/workbox-*.js`, `dist/sw.js` | MIT | Copyright 2018 Google LLC |
| [Vite](https://github.com/vitejs/vite) (modulepreload polyfill) | `dist/assets/index-*.js` | MIT | Copyright (c) 2019-present, VoidZero Inc. and Vite contributors |
| [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) | `dist/registerSW.js`, `dist/manifest.webmanifest` | MIT | Copyright (c) 2020-PRESENT Anthony Fu |
All three are distributed under the MIT License:
```
MIT License
Copyright 2018 Google LLC
Copyright (c) 2019-present, VoidZero Inc. and Vite contributors
Copyright (c) 2020-PRESENT Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## Loaded at runtime from CDNs (not redistributed)
These libraries are fetched by the browser from their CDNs and are not included in this repository. They are listed here for reference.
| Library | License |
|---|---|
| [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) (Play CDN) | MIT |
| [Alpine.js](https://github.com/alpinejs/alpine) | MIT |
| [cash-dom](https://github.com/fabiospampinato/cash) | MIT |
| [Lucide](https://github.com/lucide-icons/lucide) | ISC |
| [markdown-it](https://github.com/markdown-it/markdown-it) | MIT |
| [markdown-it-mathjax3](https://github.com/tani/markdown-it-mathjax3) | MIT |
| [highlight.js](https://github.com/highlightjs/highlight.js) | BSD-3-Clause |
| [github-markdown-css](https://github.com/sindresorhus/github-markdown-css) | MIT |
| [localForage](https://github.com/localForage/localForage) | Apache-2.0 |
| [CodeJar](https://github.com/antonmedv/codejar) | MIT |
| [tiny-ripple](https://www.npmjs.com/package/tiny-ripple) | See its LICENSE |
+37
View File
@@ -0,0 +1,37 @@
# Android app
Sune for Android is a [Trusted Web Activity](https://developer.chrome.com/docs/android/trusted-web-activity) wrapper around https://sune.chat, built with [Bubblewrap](https://github.com/GoogleChromeLabs/bubblewrap). It contains no app code, only the config in `twa-manifest.json`.
| | New app | Legacy app (v0.x) |
|---|---|---|
| Package | `chat.sune.app` | `com.planetrenox.sune` |
| Host | `sune.chat` (also trusts `sune.planetrenox.com`) | `sune.planetrenox.com` |
| Signing cert SHA-256 | `09:85:1F:7B:BB:40:67:9C:F8:70:1C:7D:9F:65:72:77:78:0E:C0:58:CF:34:09:9A:97:D2:8A:74:FB:CE:87:2A` | `E8:7D:70:60:C2:7D:EF:CB:4D:4D:2B:E2:2E:5E:37:ED:F7:0B:1E:F5:77:D7:04:1F:6B:07:EC:B8:1D:78:6D:43` (**leaked**) |
Both are listed for both domains in [`public/.well-known/assetlinks.json`](../public/.well-known/assetlinks.json); that file is what makes Chrome hide the address bar. Remove the legacy entry once nobody uses the old app (a leaked key can sign a fake `com.planetrenox.sune` that Chrome would trust).
## Releasing
Actions → **Release APK** → Run workflow on `master` → enter the tag, e.g. `v1.0.0`.
It builds `sune-v1.0.0.apk`, signs it, checks the signing cert against `assetlinks.json`, attests build provenance, then creates the tag and release. `versionCode` is `major*10000 + minor*100 + patch`. Run it from any other branch to do a dry run: it builds and verifies but publishes nothing.
## Verifying a download
```bash
gh attestation verify sune-v1.0.0.apk --repo sune-org/sune
```
This proves the file was built by this repo's workflow at the commit named in the release, and not modified since. The APK is only a wrapper; the app itself is served live from sune.chat and is not covered by it.
## Signing key
Never in the repo (`*.jks` is gitignored). Repo secrets, read only by `.github/workflows/apk.yml`:
| Secret | What |
|---|---|
| `ANDROID_KEYSTORE_B64` | base64 of the PKCS12 keystore (RSA 4096, alias `sune`, valid to 2054) |
| `ANDROID_KEYSTORE_PASS` | keystore password (also used as the key password) |
| `ANDROID_KEY_ALIAS` | `sune` |
Losing this key means users can't update `chat.sune.app` and would have to uninstall and reinstall. Keep an offline copy.
+30
View File
@@ -0,0 +1,30 @@
{
"packageId": "chat.sune.app",
"host": "sune.chat",
"name": "Sune",
"launcherName": "Sune",
"display": "standalone",
"orientation": "portrait",
"themeColor": "#FFFFFF",
"themeColorDark": "#FFFFFF",
"navigationColor": "#FFFFFF",
"navigationColorDark": "#FFFFFF",
"navigationDividerColor": "#00000000",
"navigationDividerColorDark": "#00000000",
"backgroundColor": "#000000",
"startUrl": "/",
"iconUrl": "https://sune.chat/appstore_content/%E2%9C%BA.png",
"maskableIconUrl": "https://sune.chat/appstore_content/%E2%9C%BA.png",
"webManifestUrl": "https://sune.chat/manifest.webmanifest",
"fullScopeUrl": "https://sune.chat/",
"additionalTrustedOrigins": ["sune.planetrenox.com"],
"enableNotifications": false,
"enableSiteSettingsShortcut": false,
"splashScreenFadeOutDuration": 300,
"fallbackType": "customtabs",
"minSdkVersion": 21,
"appVersion": "1.0.0",
"appVersionCode": 10000,
"signingKey": { "path": "./sune.jks", "alias": "sune" },
"generatorApp": "bubblewrap-cli"
}
+10
View File
@@ -8,5 +8,15 @@
"E8:7D:70:60:C2:7D:EF:CB:4D:4D:2B:E2:2E:5E:37:ED:F7:0B:1E:F5:77:D7:04:1F:6B:07:EC:B8:1D:78:6D:43" "E8:7D:70:60:C2:7D:EF:CB:4D:4D:2B:E2:2E:5E:37:ED:F7:0B:1E:F5:77:D7:04:1F:6B:07:EC:B8:1D:78:6D:43"
] ]
} }
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "chat.sune.app",
"sha256_cert_fingerprints": [
"09:85:1F:7B:BB:40:67:9C:F8:70:1C:7D:9F:65:72:77:78:0E:C0:58:CF:34:09:9A:97:D2:8A:74:FB:CE:87:2A"
]
}
} }
] ]
@@ -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:)/, "").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
@@ -62,7 +63,7 @@ var buildBody = () => {
if (last.role === "assistant" && last.content.length === 0 && (!last.images || last.images.length === 0)) msgs.pop(); if (last.role === "assistant" && last.content.length === 0 && (!last.images || last.images.length === 0)) msgs.pop();
} }
const b = payloadWithSampling({ const b = payloadWithSampling({
model: SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/, ""), model: SUNE.model.replace(/^(or:|oai:|g:|cla:)/, ""),
messages: msgs, messages: msgs,
stream: true stream: true
}); });
@@ -70,7 +71,6 @@ var buildBody = () => {
...SUNE.reasoning_effort && SUNE.reasoning_effort !== "default" ? { effort: SUNE.reasoning_effort } : {}, ...SUNE.reasoning_effort && SUNE.reasoning_effort !== "default" ? { effort: SUNE.reasoning_effort } : {},
exclude: !SUNE.include_thoughts exclude: !SUNE.include_thoughts
}; };
if (SUNE.verbosity) b.verbosity = SUNE.verbosity;
if (SUNE.img_output) { if (SUNE.img_output) {
b.modalities = ["image"]; b.modalities = ["image"];
b.image_config = { b.image_config = {
@@ -82,8 +82,8 @@ var buildBody = () => {
}; };
async function streamORP(body, onDelta, streamId) { async function streamORP(body, onDelta, streamId) {
const { USER, SUNE, state, gid, cacheStore } = window; const { USER, SUNE, state, gid, cacheStore } = window;
const model = SUNE.model, provider = model.startsWith("oai:") ? "openai" : model.startsWith("g:") ? "google" : model.startsWith("cla:") ? "claude" : model.startsWith("cf:") ? "cloudflare" : model.startsWith("or:") ? "openrouter" : USER.provider; const model = SUNE.model, provider = model.startsWith("oai:") ? "openai" : model.startsWith("g:") ? "google" : model.startsWith("cla:") ? "claude" : model.startsWith("or:") ? "openrouter" : USER.provider;
const apiKey = provider === "openai" ? USER.apiKeyOpenAI : provider === "google" ? USER.apiKeyGoogle : provider === "claude" ? USER.apiKeyClaude : provider === "cloudflare" ? USER.apiKeyCloudflare : USER.apiKeyOpenRouter; const apiKey = provider === "openai" ? USER.apiKeyOpenAI : provider === "google" ? USER.apiKeyGoogle : provider === "claude" ? USER.apiKeyClaude : USER.apiKeyOpenRouter;
if (!apiKey) { if (!apiKey) {
onDelta(window.localDemoReply(), true); onDelta(window.localDemoReply(), true);
return { return {
@@ -210,7 +210,7 @@ var generateTitleWithAI = async (messages) => {
role: "user", role: "user",
content: `${prePrompt}\n\n${convo}\n\n${postPrompt}` content: `${prePrompt}\n\n${convo}\n\n${postPrompt}`
}], }],
max_tokens: 6, max_tokens: 7,
temperature: .35 temperature: .35
}) })
}); });
@@ -250,7 +250,7 @@ var el = window.el = Object.fromEntries([
"set_repetition_penalty", "set_repetition_penalty",
"set_min_p", "set_min_p",
"set_top_a", "set_top_a",
"set_verbosity", "set_max_tokens",
"set_reasoning_effort", "set_reasoning_effort",
"set_system_prompt", "set_system_prompt",
"set_hide_composer", "set_hide_composer",
@@ -372,7 +372,7 @@ var asDataURL = (f) => new Promise((r) => {
fr.onload = () => r(String(fr.result || "")); fr.onload = () => r(String(fr.result || ""));
fr.readAsDataURL(f); fr.readAsDataURL(f);
}); });
var imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => { var imgToWebp = (f, D = 96, q = 75) => new Promise((r, j) => {
if (!f) return j(); if (!f) return j();
const i = new Image(); const i = new Image();
i.onload = () => { i.onload = () => {
@@ -504,14 +504,13 @@ var USER = {
}, },
get apiKey() { get apiKey() {
const p = this.provider; const p = this.provider;
return p === "openai" ? this.apiKeyOpenAI : p === "google" ? this.apiKeyGoogle : p === "claude" ? this.apiKeyClaude : p === "cloudflare" ? this.apiKeyCloudflare : this.apiKeyOpenRouter; return p === "openai" ? this.apiKeyOpenAI : p === "google" ? this.apiKeyGoogle : p === "claude" ? this.apiKeyClaude : this.apiKeyOpenRouter;
}, },
set apiKey(v) { set apiKey(v) {
const p = this.provider; const p = this.provider;
if (p === "openai") this.apiKeyOpenAI = v; if (p === "openai") this.apiKeyOpenAI = v;
else if (p === "google") this.apiKeyGoogle = v; else if (p === "google") this.apiKeyGoogle = v;
else if (p === "claude") this.apiKeyClaude = v; else if (p === "claude") this.apiKeyClaude = v;
else if (p === "cloudflare") this.apiKeyCloudflare = v;
else this.apiKeyOpenRouter = v; else this.apiKeyOpenRouter = v;
}, },
get masterPrompt() { get masterPrompt() {
@@ -646,6 +645,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", () => {
@@ -1026,8 +1030,8 @@ var defaultSettings = {
repetition_penalty: "", repetition_penalty: "",
min_p: "", min_p: "",
top_a: "", top_a: "",
verbosity: "", max_tokens: "",
reasoning_effort: "default", reasoning_effort: "",
system_prompt: "", system_prompt: "",
html: "", html: "",
extension_html: "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>", extension_html: "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",
@@ -1244,7 +1248,7 @@ var getModelShort = (m) => {
var reflectActiveSune = async () => { var reflectActiveSune = async () => {
const a = SUNE.active; const a = SUNE.active;
el.suneBtnTop.title = `Settings — ${a.name}`; el.suneBtnTop.title = `Settings — ${a.name}`;
el.suneBtnTop.innerHTML = a.avatar ? `<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>` : "✺"; el.suneBtnTop.innerHTML = a.avatar ? `<img src="${esc(a.avatar)}" alt="" class="h-10 w-10 rounded-full object-cover"/>` : "✺";
el.footer.classList.toggle("hidden", !!a.settings.hide_composer); el.footer.classList.toggle("hidden", !!a.settings.hide_composer);
await renderSuneHTML(); await renderSuneHTML();
icons(); icons();
@@ -1341,7 +1345,8 @@ var payloadWithSampling = (b) => {
frequency_penalty: num(s.frequency_penalty, null), frequency_penalty: num(s.frequency_penalty, null),
repetition_penalty: num(s.repetition_penalty, null), repetition_penalty: num(s.repetition_penalty, null),
min_p: num(s.min_p, null), min_p: num(s.min_p, null),
top_a: num(s.top_a, null) top_a: num(s.top_a, null),
max_tokens: int(s.max_tokens, null) > 0 ? int(s.max_tokens, null) : null
}; };
Object.keys(p).forEach((k) => { Object.keys(p).forEach((k) => {
const v = p[k]; const v = p[k];
@@ -1736,7 +1741,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 +1757,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 +1783,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 +1799,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 +1853,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,
@@ -1865,8 +1882,8 @@ function openSettings() {
el.set_repetition_penalty.value = s.repetition_penalty; el.set_repetition_penalty.value = s.repetition_penalty;
el.set_min_p.value = s.min_p; el.set_min_p.value = s.min_p;
el.set_top_a.value = s.top_a; el.set_top_a.value = s.top_a;
el.set_verbosity.value = s.verbosity || ""; el.set_max_tokens.value = s.max_tokens || "";
el.set_reasoning_effort.value = s.reasoning_effort || "default"; el.set_reasoning_effort.value = s.reasoning_effort || "";
el.set_system_prompt.value = s.system_prompt; el.set_system_prompt.value = s.system_prompt;
el.set_hide_composer.checked = !!s.hide_composer; el.set_hide_composer.checked = !!s.hide_composer;
el.set_img_output.checked = !!s.img_output; el.set_img_output.checked = !!s.img_output;
@@ -1921,10 +1938,10 @@ $(el.settingsForm).on("submit", async (e) => {
"frequency_penalty", "frequency_penalty",
"repetition_penalty", "repetition_penalty",
"min_p", "min_p",
"top_a" "top_a",
"max_tokens"
].forEach((k) => SUNE[k] = el[`set_${k}`].value.trim()); ].forEach((k) => SUNE[k] = el[`set_${k}`].value.trim());
SUNE.verbosity = el.set_verbosity.value || ""; SUNE.reasoning_effort = el.set_reasoning_effort.value || "";
SUNE.reasoning_effort = el.set_reasoning_effort.value || "default";
SUNE.system_prompt = el.set_system_prompt.value.trim(); SUNE.system_prompt = el.set_system_prompt.value.trim();
SUNE.hide_composer = el.set_hide_composer.checked; SUNE.hide_composer = el.set_hide_composer.checked;
SUNE.img_output = el.set_img_output.checked; SUNE.img_output = el.set_img_output.checked;
@@ -2150,17 +2167,14 @@ 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();
} else {
THREAD.list = items.map((i) => {
if (i.type === "dir") return { if (i.type === "dir") return {
id: i.name, id: i.name,
title: i.name, title: i.name,
@@ -2179,11 +2193,18 @@ var pullThreads = async () => {
status: "synced" status: "synced"
} : null; } : null;
}).filter(Boolean); }).filter(Boolean);
await THREAD.save(); 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(); 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 +2304,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();
+31 -15
View File
@@ -13,15 +13,15 @@
<script defer src="//unpkg.com/alpinejs"></script> <script defer src="//unpkg.com/alpinejs"></script>
<script type="module" crossorigin src="/assets/index-TjHmRKFx.js"></script> <script type="module" crossorigin src="/assets/index-C4jYdBxX.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')">
<div class="flex flex-col h-dvh max-h-dvh overflow-hidden"> <div class="flex flex-col h-dvh max-h-dvh overflow-hidden">
<header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200"> <header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center"> <div class="mx-auto w-full max-w-none px-4 py-2 grid grid-cols-3 items-center">
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button> <button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
<button id="suneBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings">✺</button> <button id="suneBtnTop" class="justify-self-center h-10 w-10 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings">✺</button>
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div> <div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
</div> </div>
</header> </header>
@@ -101,12 +101,12 @@
<div id="suneModal" class="hidden fixed inset-0 z-50"> <div id="suneModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div> <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="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="flex flex-col max-h-[calc(100dvh-4rem)] 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> <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"> <form id="settingsForm" class="flex flex-col min-h-0 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 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 id="panelModel" class="p-4 space-y-4 overflow-y-auto">
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div> <div class="grid grid-cols-2 gap-3"><div><div class="flex items-center gap-1 mb-1"><label for="set_model" class="text-gray-700 font-medium">Model name</label><button type="button" onclick="modelHelp.showModal()" class="p-1 -m-1 rounded-full text-gray-400 hover:text-gray-700" aria-label="How to set the model name"><i data-lucide="circle-question-mark" class="h-4 w-4"></i></button></div><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(0–2)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(0–2)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(0–1)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(0–1)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
@@ -115,7 +115,7 @@
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(0–2)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(0–2)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(0–1)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(0–1)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(0–1)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(0–1)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Verbosity</label><select id="set_verbosity" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div> <div><label class="block text-gray-700 font-medium mb-1">Max Tokens (output)</label><input id="set_max_tokens" type="number" min="1" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder=""/></div>
</div> </div>
<div class="flex flex-wrap items-center gap-2 pt-2"> <div class="flex flex-wrap items-center gap-2 pt-2">
<div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div> <div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div>
@@ -149,10 +149,11 @@
</div> </div>
</div> </div>
</div> </div>
<div id="panelPrompt" class="p-4 space-y-4 hidden"> <div id="panelPrompt" class="p-4 space-y-4 overflow-y-auto 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 overflow-y-auto 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>
<div class="pt-0"> <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" autocorrect="off" autocapitalize="off" autocomplete="off"></pre> <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" autocorrect="off" autocapitalize="off" autocomplete="off"></pre>
@@ -168,20 +169,35 @@
</div> </div>
</div> </div>
</div> </div>
<dialog id="modelHelp" onclick="event.target===this&&this.close()" class="w-[calc(100%-2rem)] max-w-sm max-h-[85dvh] p-0 rounded-2xl border border-gray-200 bg-white text-gray-900 shadow-xl backdrop:bg-black/30">
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Model name</span><button type="button" onclick="modelHelp.close()" 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>
<div class="p-4 space-y-3 text-sm text-gray-700 [&_code]:text-xs [&_code]:bg-gray-100 [&_code]:rounded [&_code]:px-1 [&_code]:py-0.5 [&_a]:underline [&_a]:text-gray-900">
<p>The <b>Provider</b> selected in <b>Account Settings → General</b> is used, unless the model name starts with one of these prefixes:</p>
<div class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1"><code>or:</code><span>OpenRouter</span><code>oai:</code><span>OpenAI</span><code>g:</code><span>Google</span><code>cla:</code><span>Claude</span></div>
<p>After the prefix, write the model slug exactly the way that provider's API expects it:</p>
<div class="space-y-2">
<div><div class="font-medium">OpenRouter <span class="font-normal text-gray-400">· vendor/model</span></div><code>or:anthropic/claude-opus-5.5</code><div class="text-xs text-gray-500 mt-0.5">Browse slugs at <a href="https://openrouter.ai/models" target="_blank" rel="noopener">openrouter.ai/models</a></div></div>
<div><div class="font-medium">OpenAI</div><code>oai:gpt-6-sol</code><div class="text-xs text-gray-500 mt-0.5"><a href="https://platform.openai.com/docs/models" target="_blank" rel="noopener">OpenAI models</a></div></div>
<div><div class="font-medium">Google</div><code>g:gemini-3.8-flash</code><div class="text-xs text-gray-500 mt-0.5"><a href="https://ai.google.dev/gemini-api/docs/models" target="_blank" rel="noopener">Gemini models</a></div></div>
<div><div class="font-medium">Claude</div><code>cla:claude-opus-5-5</code><div class="text-xs text-gray-500 mt-0.5"><a href="https://docs.anthropic.com/en/docs/about-claude/models" target="_blank" rel="noopener">Claude models</a></div></div>
</div>
<p class="text-xs text-gray-500">Tip: add <code>:online</code> to the end to enable web search, e.g. <code>g:gemini-3.8-flash:online</code>.</p>
</div>
</dialog>
<div id="accountSettingsModal" class="hidden fixed inset-0 z-50"> <div id="accountSettingsModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div> <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="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="flex flex-col max-h-[calc(100dvh-5rem)] 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> <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"> <form id="accountSettingsForm" class="flex flex-col min-h-0 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><button type="button" id="accountTabUser" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">User</button></div> <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><button type="button" id="accountTabUser" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">User</button></div>
<div id="accountPanelGeneral" class="p-4 space-y-4"> <div id="accountPanelGeneral" class="p-4 space-y-4 overflow-y-auto">
<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><option value="google">Google</option><option value="claude">Claude</option></select><p class="mt-1 text-xs text-gray-500">Or you can prefix model names with or:, oai:, g:, or cla: to override.</p></div> <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><option value="google">Google</option><option value="claude">Claude</option></select><p class="mt-1 text-xs text-gray-500">Or you can prefix model names with or:, oai:, g:, or cla: 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">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
</div> </div>
<div id="accountPanelAPI" class="p-4 hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Custom Key 1</label><div class="relative"><input id="set_api_key_custom1" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_custom1" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.customKey1</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div></div></div> <div id="accountPanelAPI" class="p-4 overflow-y-auto hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Custom Key 1</label><div class="relative"><input id="set_api_key_custom1" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_custom1" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.customKey1</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div></div></div>
<div id="accountPanelUser" class="p-4 space-y-4 hidden"> <div id="accountPanelUser" class="p-4 space-y-4 overflow-y-auto hidden">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div> <div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div>
<input id="userAvatarInput" type="file" accept="image/*" class="hidden"> <input id="userAvatarInput" type="file" accept="image/*" class="hidden">
+1 -1
View File
@@ -1 +1 @@
{"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"}]} {"name":"Sune","short_name":"Sune","description":"OpenRouter GUI Frontend","start_url":"/","display":"standalone","background_color":"#000000","theme_color":"#FFFFFF","lang":"en","scope":"/","id":"/","orientation":"portrait","categories":["productivity","utilities"],"icons":[{"src":"/appstore_content/✺.png","sizes":"1024x1024","type":"image/png"}],"screenshots":[{"src":"/appstore_content/screenshot1.jpg","sizes":"1344x2693","type":"image/jpeg"},{"src":"/appstore_content/screenshot2.jpg","sizes":"1344x2699","type":"image/jpeg"}]}
Vendored
+1 -1
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} didn’t register its module`);return e}));self.define=(n,r)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const l=e=>i(e,t),c={module:{uri:t},exports:o,require:l};s[t]=Promise.all(n.map(e=>c[e]||l(e))).then(e=>(r(...e),o))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"index.html",revision:"a64ac52329387d11ea67cf7b0a2b461d"},{url:"assets/index-TjHmRKFx.js",revision:null},{url:"assets/index-DUC1RW1F.css",revision:null},{url:"manifest.webmanifest",revision:"7a6c5c6ab9cb5d3605d21df44c6b17a2"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))}); if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t 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 d={};const o=e=>i(e,t),c={module:{uri:t},exports:d,require:o};s[t]=Promise.all(n.map(e=>c[e]||o(e))).then(e=>(r(...e),d))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"1872c500de691dce40960bb85481de07"},{url:"index.html",revision:"9c9e0b7dd947ef0c88f648f8885fcfbb"},{url:"assets/index-DUC1RW1F.css",revision:null},{url:"assets/index-C4jYdBxX.js",revision:null},{url:"appstore_content/✺.png",revision:"06dd8ca13016278fc47e9a226acdb7f9"},{url:"manifest.webmanifest",revision:"70d853d7dcb45ddd8488db46950b4b33"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("index.html")))});
+10
View File
@@ -8,5 +8,15 @@
"E8:7D:70:60:C2:7D:EF:CB:4D:4D:2B:E2:2E:5E:37:ED:F7:0B:1E:F5:77:D7:04:1F:6B:07:EC:B8:1D:78:6D:43" "E8:7D:70:60:C2:7D:EF:CB:4D:4D:2B:E2:2E:5E:37:ED:F7:0B:1E:F5:77:D7:04:1F:6B:07:EC:B8:1D:78:6D:43"
] ]
} }
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "chat.sune.app",
"sha256_cert_fingerprints": [
"09:85:1F:7B:BB:40:67:9C:F8:70:1C:7D:9F:65:72:77:78:0E:C0:58:CF:34:09:9A:97:D2:8A:74:FB:CE:87:2A"
]
}
} }
] ]
+19 -16
View File
@@ -1,30 +1,31 @@
[![Download For Android (.apk)](https://img.shields.io/badge/Download-For%20Android%20(.apk)-green?style=for-the-badge&logo=android)](https://github.com/multipleof4/sune/releases/download/v0.23.0/sune-v0.23.0.apk) [![Download For Android (.apk)](https://img.shields.io/badge/Download-For%20Android%20(.apk)-green?style=for-the-badge&logo=android)](https://github.com/multipleof4/sune/releases/download/v0.23.0/sune-v0.23.0.apk)
![Main](./public/appstore_content/screenshot1.jpg) <p align="center">
<img src="./public/appstore_content/screenshot1.jpg" alt="Main" width="280" />
</p>
> Each sune is like a module. You can have many. And share them. > Each sune is like a module. You can have many. And share them.
![Sunes](./public/appstore_content/sune_sidebar.png) <p align="center">
<img src="./public/appstore_content/sune_sidebar.png" alt="Sunes" width="280" />
![Setting](./public/appstore_content/screenshot3.jpg) <img src="./public/appstore_content/screenshot3.jpg" alt="Setting" width="280" />
</p>
💠 New! 💠 New!
> You can have scripts which run on the page of each sune — either to function call or extend functionality of the app or sune. > You can have scripts which run on the page of each sune — either to function call or extend functionality of the app or sune.
![Scripting](./public/appstore_content/screenshot4.jpg) <p align="center">
<img src="./public/appstore_content/screenshot4.jpg" alt="Scripting" width="280" />
<img src="./public/appstore_content/screenshot_miku.png" alt="Miku" width="280" />
</p>
> Image support. > There is a marketplace and LaTeX support out of the box.
![Miku](./public/appstore_content/screenshot_miku.png) <p align="center">
<img src="./public/appstore_content/screenshot_marketplace.jpg" alt="Marketplace" width="280" />
> There is a marketplace. <img src="./public/appstore_content/latex.png" alt="LaTeX" width="280" />
</p>
![Marketplace](./public/appstore_content/screenshot_marketplace.jpg)
> LaTeX support out of the box.
![LaTeX](./public/appstore_content/latex.png)
--- ---
@@ -32,7 +33,9 @@
Never lose a conversation again. Sune can sync all your threads to a GitHub repo. Never lose a conversation again. Sune can sync all your threads to a GitHub repo.
![Sync](./public/appstore_content/sync.png) <p align="center">
<img src="./public/appstore_content/sync.png" alt="Sync" width="320" />
</p>
### Setup for Syncing Chats ### Setup for Syncing Chats
+1 -1
View File
@@ -3,7 +3,7 @@ export const el = window.el = Object.fromEntries(
'topbar','chat','messages','composer','input','sendBtn','suneBtnTop','suneModal','suneURL', 'topbar','chat','messages','composer','input','sendBtn','suneBtnTop','suneModal','suneURL',
'settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript', 'settingsForm','closeSettings','cancelSettings','tabModel','tabPrompt','tabScript',
'panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k', 'panelModel','panelPrompt','panelScript','set_model','set_temperature','set_top_p','set_top_k',
'set_frequency_penalty','set_repetition_penalty','set_min_p','set_top_a','set_verbosity', 'set_frequency_penalty','set_repetition_penalty','set_min_p','set_top_a','set_max_tokens',
'set_reasoning_effort','set_system_prompt','set_hide_composer','set_include_thoughts', 'set_reasoning_effort','set_system_prompt','set_hide_composer','set_include_thoughts',
'set_img_output','set_aspect_ratio','set_image_size','aspectRatioContainer', 'set_img_output','set_aspect_ratio','set_image_size','aspectRatioContainer',
'set_ignore_master_prompt','deleteSuneBtn','sidebarLeft','sidebarOverlayLeft','sidebarBtnLeft', 'set_ignore_master_prompt','deleteSuneBtn','sidebarLeft','sidebarOverlayLeft','sidebarBtnLeft',
+5
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 }));
} }
+40 -10
View File
@@ -57,14 +57,14 @@ const suneStorage={
} }
const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}} const su={key:'sunes_v1',activeKey:'active_sune_id',load(){try{return JSON.parse(localStorage.getItem(this.key)||'[]')}catch{return[]}},save(list){localStorage.setItem(this.key,JSON.stringify(list||[]))},getActiveId(){return localStorage.getItem(this.activeKey)||null},setActiveId(id){localStorage.setItem(this.activeKey,id||'')}}
const defaultSettings={model:DEFAULT_MODEL,temperature:'',top_p:'',top_k:'',frequency_penalty:'',repetition_penalty:'',min_p:'',top_a:'',verbosity:'',reasoning_effort:'default',system_prompt:'',html:'',extension_html:"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",hide_composer:false,include_thoughts:false,img_output:false,aspect_ratio:'1:1',image_size:'1K',ignore_master_prompt:false} const defaultSettings={model:DEFAULT_MODEL,temperature:'',top_p:'',top_k:'',frequency_penalty:'',repetition_penalty:'',min_p:'',top_a:'',max_tokens:'',reasoning_effort:'',system_prompt:'',html:'',extension_html:"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>",hide_composer:false,include_thoughts:false,img_output:false,aspect_ratio:'1:1',image_size:'1K',ignore_master_prompt:false}
const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',url:p.url||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{})}) const makeSune=(p={})=>({id:p.id||gid(),name:p.name?.trim()||'Default',pinned:!!p.pinned,avatar:p.avatar||'',url:p.url||'',updatedAt:p.updatedAt||Date.now(),settings:Object.assign({},defaultSettings,p.settings||{})})
let sunes=(su.load()||[]).map(makeSune) let sunes=(su.load()||[]).map(makeSune)
const SUNE=window.SUNE=new Proxy({storage:suneStorage,get list(){return sunes},get id(){return su.getActiveId()},get active(){return sunes.find(a=>a.id===su.getActiveId())||sunes[0]},get:id=>sunes.find(s=>s.id===id),setActive:id=>su.setActiveId(id||''),create(p={}){const s=makeSune(p);sunes.unshift(s);su.save(sunes);markLocalDirty();return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);cleanSuneStorage(id);gcStorage();markLocalDirty();if(sunes.length===0){const def=this.create({name:'Default'});this.setActive(def.id)}else if(curId===id)this.setActive(sunes[0].id)},save:()=>su.save(sunes)},{get(t,p){if(p==='fetchDotSune')return async g=>{try{const u=g.startsWith('http')?g:(()=>{const[a,b]=g.split('@'),[c,d]=a.split('/'),[e,...f]=b.split('/');return`https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join('/')}`})(),j=await(await fetch(u)).json(),l=sunes.length;sunes.unshift(...(Array.isArray(j)?j:j?.sunes||[]).filter(s=>s?.id&&!t.get(s.id)).map(s=>makeSune(s)));sunes.length>l&&t.save()}catch{}};if(p==='attach')return async files=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser('(attachments)');addMessage({role:'assistant',content:clean,...activeMeta()});await THREAD.persist()};if(p==='log')return async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await THREAD.persist()};if(p==='lastReply')return [...state.messages].reverse().find(m=>m.role==='assistant');if(p==='infer')return async()=>{if(state.busy||!SUNE.model||state.abortRequested){state.abortRequested=false;return};await ensureThreadOnFirstUser('Sune Inference');const th=THREAD.active;if(th&&!th.title)(async()=>THREAD.setTitle(th.id,await generateTitleWithAI(state.messages)||'Sune Inference'))();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:null,bubble:null,meta:null,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;assistantMsg.content[0].text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId)};if(p==='getByName')return n=>sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(p==='handoff')return async n=>{await new Promise(r=>setTimeout(r,4000));const s=sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(!s)return;SUNE.setActive(s.id);renderSidebar();await reflectActiveSune();await SUNE.infer()};if(p in t)return t[p];const a=t.active;if(!a)return;if(p in a.settings)return a.settings[p];if(p in a)return a[p]},set(t,p,v){const a=t.active;if(!a)return false;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return false;const isTopLevel=/^(name|avatar|url|pinned)$/.test(p),target=isTopLevel?sunes[i]:sunes[i].settings;let value=v;if(!isTopLevel){if(p==='system_prompt')value=v||''}if(target[p]!==value){target[p]=value;sunes[i].updatedAt=Date.now();su.save(sunes);markLocalDirty()}return true}}) const SUNE=window.SUNE=new Proxy({storage:suneStorage,get list(){return sunes},get id(){return su.getActiveId()},get active(){return sunes.find(a=>a.id===su.getActiveId())||sunes[0]},get:id=>sunes.find(s=>s.id===id),setActive:id=>su.setActiveId(id||''),create(p={}){const s=makeSune(p);sunes.unshift(s);su.save(sunes);markLocalDirty();return s},delete(id){const curId=this.id;sunes=sunes.filter(s=>s.id!==id);su.save(sunes);cleanSuneStorage(id);gcStorage();markLocalDirty();if(sunes.length===0){const def=this.create({name:'Default'});this.setActive(def.id)}else if(curId===id)this.setActive(sunes[0].id)},save:()=>su.save(sunes)},{get(t,p){if(p==='fetchDotSune')return async g=>{try{const u=g.startsWith('http')?g:(()=>{const[a,b]=g.split('@'),[c,d]=a.split('/'),[e,...f]=b.split('/');return`https://raw.githubusercontent.com/${c}/${d}/${e}/${f.join('/')}`})(),j=await(await fetch(u)).json(),l=sunes.length;sunes.unshift(...(Array.isArray(j)?j:j?.sunes||[]).filter(s=>s?.id&&!t.get(s.id)).map(s=>makeSune(s)));sunes.length>l&&t.save()}catch{}};if(p==='attach')return async files=>{const arr=[];for(const f of files||[])arr.push(await toAttach(f));const clean=arr.filter(Boolean);if(!clean.length)return;await ensureThreadOnFirstUser('(attachments)');addMessage({role:'assistant',content:clean,...activeMeta()});await THREAD.persist()};if(p==='log')return async s=>{const t=String(s??'').trim();if(!t)return;await ensureThreadOnFirstUser(t);addMessage({role:'assistant',content:[{type:'text',text:t}],...activeMeta()});await THREAD.persist()};if(p==='lastReply')return [...state.messages].reverse().find(m=>m.role==='assistant');if(p==='infer')return async()=>{if(state.busy||!SUNE.model||state.abortRequested){state.abortRequested=false;return};await ensureThreadOnFirstUser('Sune Inference');const th=THREAD.active;if(th&&!th.title)(async()=>THREAD.setTitle(th.id,await generateTitleWithAI(state.messages)||'Sune Inference'))();state.busy=true;setBtnStop();const a=SUNE.active,suneMeta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},streamId=sid(),suneBubble=addSuneBubbleStreaming(suneMeta,streamId);suneBubble.dataset.mid=streamId;suneBubble.innerHTML=SUNE_LOGO_SVG;const assistantMsg=Object.assign({id:streamId,role:'assistant',content:[{type:'text',text:''}]},suneMeta);state.messages.push(assistantMsg);THREAD.persist(false);state.stream={rid:null,bubble:null,meta:null,text:'',done:false};let buf='',completed=false;const onDelta=(delta,done,imgs)=>{if(imgs){if(!assistantMsg.images)assistantMsg.images=[];assistantMsg.images.push(...imgs)}buf+=delta;state.stream.text=buf;assistantMsg.content[0].text=buf;renderMarkdown(suneBubble,partsToText(assistantMsg),{enhance:false});if(done&&!completed){completed=true;setBtnSend();state.busy=false;enhanceCodeBlocks(suneBubble,true);THREAD.persist(true);el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:assistantMsg}}));state.stream={rid:null,bubble:null,meta:null,text:'',done:false}}else if(!done)THREAD.persist(false)};await streamChat(onDelta,streamId)};if(p==='getByName')return n=>sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(p==='handoff')return async n=>{await new Promise(r=>setTimeout(r,4000));const s=sunes.find(s=>s.name.toLowerCase()===(n||'').trim().toLowerCase());if(!s)return;SUNE.setActive(s.id);renderSidebar();await reflectActiveSune();await SUNE.infer()};if(p in t)return t[p];const a=t.active;if(!a)return;if(p in a.settings)return a.settings[p];if(p in a)return a[p]},set(t,p,v){const a=t.active;if(!a)return false;const i=sunes.findIndex(s=>s.id===a.id);if(i<0)return false;const isTopLevel=/^(name|avatar|url|pinned)$/.test(p),target=isTopLevel?sunes[i]:sunes[i].settings;let value=v;if(!isTopLevel){if(p==='system_prompt')value=v||''}if(target[p]!==value){target[p]=value;sunes[i].updatedAt=Date.now();su.save(sunes);markLocalDirty()}return true}})
if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)} if(!sunes.length){const def=SUNE.create({name:'Default'});SUNE.setActive(def.id)}
const state=window.state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:false}} const state=window.state={messages:[],busy:false,controller:null,currentThreadId:null,abortRequested:false,attachments:[],stream:{rid:null,bubble:null,meta:null,text:'',done:false}}
const getModelShort=m=>{const mm=m||SUNE.model||'';return mm.includes('/')?mm.split('/').pop():mm} const getModelShort=m=>{const mm=m||SUNE.model||'';return mm.includes('/')?mm.split('/').pop():mm}
const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.title=`Settings — ${a.name}`;el.suneBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:'✺';el.footer.classList.toggle('hidden',!!a.settings.hide_composer);await renderSuneHTML();icons()} const reflectActiveSune=async()=>{const a=SUNE.active;el.suneBtnTop.title=`Settings — ${a.name}`;el.suneBtnTop.innerHTML=a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-10 w-10 rounded-full object-cover"/>`:'✺';el.footer.classList.toggle('hidden',!!a.settings.hide_composer);await renderSuneHTML();icons()}
const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===SUNE.id?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.id}" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center" title="More"><i data-lucide="more-horizontal" class="h-4 w-4"></i></button></div>` const suneRow=a=>`<div class="relative flex items-center gap-2 px-3 py-2 ${a.pinned?'bg-yellow-50':''}"><button data-sune-id="${a.id}" class="flex-1 text-left flex items-center gap-2 ${a.id===SUNE.id?'font-medium':''}">${a.avatar?`<img src="${esc(a.avatar)}" alt="" class="h-8 w-8 rounded-full object-cover"/>`:`<span class="h-6 w-6 rounded-full bg-gray-200 flex items-center justify-center">✺</span>`}<span class="truncate">${a.pinned?'📌 ':''}${esc(a.name)}</span></button><button data-sune-menu="${a.id}" class="h-8 w-8 rounded hover:bg-gray-100 flex items-center justify-center" title="More"><i data-lucide="more-horizontal" class="h-4 w-4"></i></button></div>`
const renderSidebar=window.renderSidebar=()=>{const list=[...SUNE.list].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()} const renderSidebar=window.renderSidebar=()=>{const list=[...SUNE.list].sort((a,b)=>(b.pinned-a.pinned));el.suneList.innerHTML=list.map(suneRow).join('');icons()}
const renderUserUI=window.renderUserUI=()=>{if(!el.userMenuAvatar)return;const a=USER.avatar;if(a){el.userMenuAvatar.className='h-6 w-6 rounded-full overflow-hidden shrink-0';el.userMenuAvatar.innerHTML=`<img src="${esc(a)}" class="h-full w-full object-cover"/>`}else{el.userMenuAvatar.className='h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center shrink-0 text-xs';el.userMenuAvatar.textContent='👤'}} const renderUserUI=window.renderUserUI=()=>{if(!el.userMenuAvatar)return;const a=USER.avatar;if(a){el.userMenuAvatar.className='h-6 w-6 rounded-full overflow-hidden shrink-0';el.userMenuAvatar.innerHTML=`<img src="${esc(a)}" class="h-full w-full object-cover"/>`}else{el.userMenuAvatar.className='h-6 w-6 rounded-full bg-gray-900 text-white flex items-center justify-center shrink-0 text-xs';el.userMenuAvatar.textContent='👤'}}
@@ -74,7 +74,7 @@ function msgRow(m){const $row=_createMessageRow(m);$(el.messages).append($row);q
const addMessage=window.addMessage=function(m,track=true){m.id=m.id||gid();if(!Array.isArray(m.content)&&m.content!=null){m.content=[{type:'text',text:String(m.content)}]}const bubble=msgRow(m);bubble.dataset.mid=m.id;renderMarkdown(bubble,partsToText(m));if(track)state.messages.push(m);if(m.role==='assistant')el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:m}}));return bubble} const addMessage=window.addMessage=function(m,track=true){m.id=m.id||gid();if(!Array.isArray(m.content)&&m.content!=null){m.content=[{type:'text',text:String(m.content)}]}const bubble=msgRow(m);bubble.dataset.mid=m.id;renderMarkdown(bubble,partsToText(m));if(track)state.messages.push(m);if(m.role==='assistant')el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:m}}));return bubble}
const addSuneBubbleStreaming=(meta,id)=>msgRow(Object.assign({role:'assistant',id},meta)) const addSuneBubbleStreaming=(meta,id)=>msgRow(Object.assign({role:'assistant',id},meta))
const clearChat=()=>{el.suneHtml.dispatchEvent(new CustomEvent('sune:unmount'));state.messages=[];el.messages.innerHTML='';state.attachments=[];updateAttachBadge();el.fileInput.value=''} const clearChat=()=>{el.suneHtml.dispatchEvent(new CustomEvent('sune:unmount'));state.messages=[];el.messages.innerHTML='';state.attachments=[];updateAttachBadge();el.fileInput.value=''}
const payloadWithSampling=b=>{const o=Object.assign({},b),s=SUNE,p={temperature:num(s.temperature,null),top_p:num(s.top_p,null),top_k:int(s.top_k,null),frequency_penalty:num(s.frequency_penalty,null),repetition_penalty:num(s.repetition_penalty,null),min_p:num(s.min_p,null),top_a:num(s.top_a,null)};Object.keys(p).forEach(k=>{const v=p[k];if(v!==null)o[k]=v});return o} const payloadWithSampling=b=>{const o=Object.assign({},b),s=SUNE,p={temperature:num(s.temperature,null),top_p:num(s.top_p,null),top_k:int(s.top_k,null),frequency_penalty:num(s.frequency_penalty,null),repetition_penalty:num(s.repetition_penalty,null),min_p:num(s.min_p,null),top_a:num(s.top_a,null),max_tokens:int(s.max_tokens,null)>0?int(s.max_tokens,null):null};Object.keys(p).forEach(k=>{const v=p[k];if(v!==null)o[k]=v});return o}
function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.();state.busy=false;setBtnSend()}} function setBtnStop(){const b=el.sendBtn;b.dataset.mode='stop';b.type='button';b.setAttribute('aria-label','Stop');b.innerHTML='<i data-lucide="square" class="h-5 w-5"></i>';icons();b.onclick=()=>{state.abortRequested=true;state.controller?.abort?.();state.busy=false;setBtnSend()}}
function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null} function setBtnSend(){const b=el.sendBtn;b.dataset.mode='send';b.type='submit';b.setAttribute('aria-label','Send');b.innerHTML='<i data-lucide="sparkles" class="h-5 w-5"></i>';icons();b.onclick=null}
function localDemoReply(){return 'Tip: open the sidebar → User to set your API key.'} function localDemoReply(){return 'Tip: open the sidebar → User to set your API key.'}
@@ -113,15 +113,24 @@ $(el.threadList).on('scroll',()=>{
} }
isAddingThreads=false; isAddingThreads=false;
}); });
$(el.threadPopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=THREAD.get(menuThreadId);if(!th)return;const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';if(act==='pin'){th.pinned=!th.pinned;if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}}else if(act==='duplicate'){const newId=gid(),msgs=await localforage.getItem(prefix+th.id)||[];const newTh={...th,id:newId,title:th.title+' (Copy)',updatedAt:Date.now()};if(u.startsWith('gh://'))newTh.status='new';THREAD.list.unshift(newTh);await localforage.setItem(prefix+newId,msgs);await THREAD.save();await renderThreads()}else if(act==='delete'){if(confirm('Delete this chat?')){if(u.startsWith('gh://')){th.status='deleted';th.updatedAt=Date.now()}else{THREAD.list=THREAD.list.filter(x=>!th.id!==th.id);await localforage.removeItem(prefix+th.id)}if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=await localforage.getItem(prefix+th.id)||[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m)||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}else if(act==='export'){const msgs=await localforage.getItem(prefix+th.id)||[];dl(`thread-${(th.title||'thread').replace(/\W/g,'_')}-${ts()}.json`,{...th,messages:msgs})}else if(act==='copy_path'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);if(await copyToClipboard(`${info.owner}/${info.repo}@${info.branch}/${th.id}`))alert('Path copied.')}}hideThreadPopover();await THREAD.save();renderThreads()}) $(el.threadPopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuThreadId)return;const th=THREAD.get(menuThreadId);if(!th)return;const u=el.threadRepoInput.value.trim(),prefix=u.startsWith('gh://')?'rem_t_':'t_';if(act==='pin'){th.pinned=!th.pinned;if(u.startsWith('gh://')&&th.status!=='new')th.status='modified';}else if(act==='rename'){const nv=prompt('Rename to:',th.title);if(nv!=null){th.title=titleFrom(nv);th.updatedAt=Date.now();if(u.startsWith('gh://')&&th.status!=='new')th.status='modified'}}else if(act==='duplicate'){const newId=gid(),msgs=await localforage.getItem(prefix+th.id)||[];const newTh={...th,id:newId,title:th.title+' (Copy)',updatedAt:Date.now()};if(u.startsWith('gh://'))newTh.status='new';THREAD.list.unshift(newTh);await localforage.setItem(prefix+newId,msgs);await THREAD.save();await renderThreads()}else if(act==='delete'){if(confirm('Delete this chat?')){if(u.startsWith('gh://')){th.status='deleted';th.updatedAt=Date.now()}else{THREAD.list=THREAD.list.filter(x=>!th.id!==th.id);await localforage.removeItem(prefix+th.id)}if(state.currentThreadId===th.id){state.currentThreadId=null;clearChat()}}}else if(act==='count_tokens'){const msgs=await localforage.getItem(prefix+th.id)||[];let totalChars=0;for(const m of msgs){if(!m||!m.role||m.role==='system')continue;totalChars+=String(partsToText(m)||'').length}const tokens=Math.max(0,Math.ceil(totalChars/4));const k=tokens>=1000?Math.round(tokens/1000)+'k':String(tokens);alert(tokens+' tokens ('+k+')')}else if(act==='export'){const msgs=await localforage.getItem(prefix+th.id)||[];dl(`thread-${(th.title||'thread').replace(/\W/g,'_')}-${ts()}.json`,{...th,messages:msgs})}else if(act==='copy_path'){const u=el.threadRepoInput.value.trim();if(u.startsWith('gh://')){const info=parseGhUrl(u);if(await copyToClipboard(`${info.owner}/${info.repo}@${info.branch}/${th.id}`))alert('Path copied.')}}hideThreadPopover();await THREAD.save();renderThreads()})
$(el.suneList).on('click',async e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSunePopover(menuBtn,menuBtn.getAttribute('[data-sune-menu]')?menuBtn.getAttribute('[data-sune-menu]'):menuBtn.getAttribute('data-sune-menu'));return}const btn=e.target.closest('[data-sune-id]');if(!btn)return;const id=btn.getAttribute('data-sune-id');if(id){if(state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=false;state.controller=null};SUNE.setActive(id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')}}) $(el.suneList).on('click',async e=>{const menuBtn=e.target.closest('[data-sune-menu]');if(menuBtn){e.stopPropagation();showSunePopover(menuBtn,menuBtn.getAttribute('[data-sune-menu]')?menuBtn.getAttribute('[data-sune-menu]'):menuBtn.getAttribute('data-sune-menu'));return}const btn=e.target.closest('[data-sune-id]');if(!btn)return;const id=btn.getAttribute('data-sune-id');if(id){if(state.busy){state.controller?.disconnect?.();setBtnSend();state.busy=false;state.controller=null};SUNE.setActive(id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')}})
$(el.sunePopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=SUNE.get(menuSuneId);if(!s)return;const updateAndRender=async()=>{s.updatedAt=Date.now();SUNE.save();renderSidebar();await reflectActiveSune();markLocalDirty()};if(act==='pin'){s.pinned=!s.pinned;await updateAndRender()}else if(act==='rename'){const n=prompt('Rename sune to:',s.name);if(n!=null){s.name=n.trim();await updateAndRender()}}else if(act==='pfp'){const i=document.createElement('input');i.type='file';i.accept='image/*';i.onchange=async()=>{const f=i.files?.[0];if(!f)return;try{s.avatar=await imgToWebp(f);await updateAndRender()}catch{}};i.click()}else if(act==='export')dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${ts()}.sune`,[s]);hideSunePopover()}) $(el.sunePopover).on('click',async e=>{const act=e.target.closest('[data-action]')?.getAttribute('data-action');if(!act||!menuSuneId)return;const s=SUNE.get(menuSuneId);if(!s)return;const updateAndRender=async()=>{s.updatedAt=Date.now();SUNE.save();renderSidebar();await reflectActiveSune();markLocalDirty()};if(act==='pin'){s.pinned=!s.pinned;await updateAndRender()}else if(act==='rename'){const n=prompt('Rename sune to:',s.name);if(n!=null){s.name=n.trim();await updateAndRender()}}else if(act==='pfp'){const i=document.createElement('input');i.type='file';i.accept='image/*';i.onchange=async()=>{const f=i.files?.[0];if(!f)return;try{s.avatar=await imgToWebp(f);await updateAndRender()}catch{}};i.click()}else if(act==='export')dl(`sune-${(s.name||'sune').replace(/\W/g,'_')}-${ts()}.sune`,[s]);hideSunePopover()})
$(el.sunesSyncUploadBtn).on('click',()=>{hideSunesSyncPopover();performSuneUpload()}); $(el.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;
@@ -135,7 +144,7 @@ const ensureJars=async()=>{
return jars; return jars;
} }
let openedHTML=false let openedHTML=false
function openSettings(){const a=SUNE.active,s=a.settings;openedHTML=false;el.suneURL.value=a.url||'';el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_verbosity.value=s.verbosity||'';el.set_reasoning_effort.value=s.reasoning_effort||'default';el.set_system_prompt.value=s.system_prompt;el.set_hide_composer.checked=!!s.hide_composer;el.set_img_output.checked=!!s.img_output;el.set_aspect_ratio.value=s.aspect_ratio||'1:1';el.set_image_size.value=s.image_size||'1K';el.aspectRatioContainer.classList.toggle('hidden',!s.img_output);el.set_include_thoughts.checked=!!s.include_thoughts;el.set_ignore_master_prompt.checked=!!s.ignore_master_prompt;showTab('Model');el.suneModal.classList.remove('hidden')} function openSettings(){const a=SUNE.active,s=a.settings;openedHTML=false;el.suneURL.value=a.url||'';el.set_model.value=s.model;el.set_temperature.value=s.temperature;el.set_top_p.value=s.top_p;el.set_top_k.value=s.top_k;el.set_frequency_penalty.value=s.frequency_penalty;el.set_repetition_penalty.value=s.repetition_penalty;el.set_min_p.value=s.min_p;el.set_top_a.value=s.top_a;el.set_max_tokens.value=s.max_tokens||'';el.set_reasoning_effort.value=s.reasoning_effort||'';el.set_system_prompt.value=s.system_prompt;el.set_hide_composer.checked=!!s.hide_composer;el.set_img_output.checked=!!s.img_output;el.set_aspect_ratio.value=s.aspect_ratio||'1:1';el.set_image_size.value=s.image_size||'1K';el.aspectRatioContainer.classList.toggle('hidden',!s.img_output);el.set_include_thoughts.checked=!!s.include_thoughts;el.set_ignore_master_prompt.checked=!!s.ignore_master_prompt;showTab('Model');el.suneModal.classList.remove('hidden')}
const closeSettings=()=>{el.suneModal.classList.add('hidden')} const closeSettings=()=>{el.suneModal.classList.add('hidden')}
const tabs={Model:['tabModel','panelModel'],Prompt:['tabPrompt','panelPrompt'],Script:['tabScript','panelScript']} const tabs={Model:['tabModel','panelModel'],Prompt:['tabPrompt','panelPrompt'],Script:['tabScript','panelScript']}
function showTab(key){Object.entries(tabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)});if(key==='Script'){openedHTML=true;showHtmlTab('index');ensureJars().then(({html,extension})=>{const s=SUNE.settings;html.updateCode(s.html||'');extension.updateCode(s.extension_html||'')})}} function showTab(key){Object.entries(tabs).forEach(([k,[tb,pn]])=>{el[tb].classList.toggle('border-black',k===key);el[pn].classList.toggle('hidden',k!==key)});if(key==='Script'){openedHTML=true;showHtmlTab('index');ensureJars().then(({html,extension})=>{const s=SUNE.settings;html.updateCode(s.html||'');extension.updateCode(s.extension_html||'')})}}
@@ -146,7 +155,7 @@ $(el.tabModel).on('click',()=>showTab('Model'))
$(el.tabPrompt).on('click',()=>showTab('Prompt')) $(el.tabPrompt).on('click',()=>showTab('Prompt'))
$(el.tabScript).on('click',()=>showTab('Script')) $(el.tabScript).on('click',()=>showTab('Script'))
$(el.set_img_output).on('change',e=>el.aspectRatioContainer.classList.toggle('hidden',!e.target.checked)) $(el.set_img_output).on('change',e=>el.aspectRatioContainer.classList.toggle('hidden',!e.target.checked))
$(el.settingsForm).on('submit',async e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a'].forEach(k=>SUNE[k]=el[`set_${k}`].value.trim());SUNE.verbosity=(el.set_verbosity.value||'');SUNE.reasoning_effort=(el.set_reasoning_effort.value||'default');SUNE.system_prompt=el.set_system_prompt.value.trim();SUNE.hide_composer=el.set_hide_composer.checked;SUNE.img_output=el.set_img_output.checked;SUNE.aspect_ratio=el.set_aspect_ratio.value;SUNE.image_size=el.set_image_size.value;SUNE.include_thoughts=el.set_include_thoughts.checked;SUNE.ignore_master_prompt=el.set_ignore_master_prompt.checked;if(openedHTML){SUNE.html=jars.html.toString();SUNE.extension_html=jars.extension.toString()}closeSettings();await reflectActiveSune()}) $(el.settingsForm).on('submit',async e=>{e.preventDefault();SUNE.url=(el.suneURL.value||'').trim();SUNE.model=(el.set_model.value||'').trim();['temperature','top_p','top_k','frequency_penalty','repetition_penalty','min_p','top_a','max_tokens'].forEach(k=>SUNE[k]=el[`set_${k}`].value.trim());SUNE.reasoning_effort=(el.set_reasoning_effort.value||'');SUNE.system_prompt=el.set_system_prompt.value.trim();SUNE.hide_composer=el.set_hide_composer.checked;SUNE.img_output=el.set_img_output.checked;SUNE.aspect_ratio=el.set_aspect_ratio.value;SUNE.image_size=el.set_image_size.value;SUNE.include_thoughts=el.set_include_thoughts.checked;SUNE.ignore_master_prompt=el.set_ignore_master_prompt.checked;if(openedHTML){SUNE.html=jars.html.toString();SUNE.extension_html=jars.extension.toString()}closeSettings();await reflectActiveSune()})
$(el.deleteSuneBtn).on('click',async()=>{const activeId=SUNE.id,name=SUNE.name||'this sune';if(!confirm(`Delete "${name}"?`))return;SUNE.delete(activeId);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()}) $(el.deleteSuneBtn).on('click',async()=>{const activeId=SUNE.id,name=SUNE.name||'this sune';if(!confirm(`Delete "${name}"?`))return;SUNE.delete(activeId);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();closeSettings()})
$(el.newSuneBtn).on('click',async()=>{const name=prompt('Name your sune:');if(!name)return;const sune=SUNE.create({name:name.trim()});SUNE.setActive(sune.id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')}) $(el.newSuneBtn).on('click',async()=>{const name=prompt('Name your sune:');if(!name)return;const sune=SUNE.create({name:name.trim()});SUNE.setActive(sune.id);renderSidebar();await reflectActiveSune();state.currentThreadId=null;clearChat();document.getElementById('sidebarLeft').classList.add('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.add('hidden')})
let importMode=null let importMode=null
@@ -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()=>{
+28 -12
View File
@@ -20,12 +20,12 @@
<div id="suneModal" class="hidden fixed inset-0 z-50"> <div id="suneModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div> <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="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="flex flex-col max-h-[calc(100dvh-4rem)] 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> <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"> <form id="settingsForm" class="flex flex-col min-h-0 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 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 id="panelModel" class="p-4 space-y-4 overflow-y-auto">
<div class="grid grid-cols-2 gap-3"><div><label class="block text-gray-700 font-medium mb-1">Model name</label><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="default">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div> <div class="grid grid-cols-2 gap-3"><div><div class="flex items-center gap-1 mb-1"><label for="set_model" class="text-gray-700 font-medium">Model name</label><button type="button" onclick="modelHelp.showModal()" class="p-1 -m-1 rounded-full text-gray-400 hover:text-gray-700" aria-label="How to set the model name"><i data-lucide="circle-question-mark" class="h-4 w-4"></i></button></div><input id="set_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="google/gemini-3-pro-preview"/></div><div><label class="block text-gray-700 font-medium mb-1">Reasoning Effort</label><select id="set_reasoning_effort" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="none">None</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div></div>
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(0–2)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Temperature <span class="text-gray-400">(0–2)</span></label><input id="set_temperature" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(0–1)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Top P <span class="text-gray-400">(0–1)</span></label><input id="set_top_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
@@ -34,7 +34,7 @@
<div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(0–2)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Repetition Penalty <span class="text-gray-400">(0–2)</span></label><input id="set_repetition_penalty" type="number" min="0" max="2" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="1.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(0–1)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Min P <span class="text-gray-400">(0–1)</span></label><input id="set_min_p" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(0–1)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div> <div><label class="block text-gray-700 font-medium mb-1">Top A <span class="text-gray-400">(0–1)</span></label><input id="set_top_a" type="number" min="0" max="1" step="0.01" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="0.0"/></div>
<div><label class="block text-gray-700 font-medium mb-1">Verbosity</label><select id="set_verbosity" class="w-full rounded-xl border border-gray-300 px-3 py-2"><option value="">Omitted</option><option value="low">Low</option><option value="medium">Medium</option><option value="high">High</option></select></div> <div><label class="block text-gray-700 font-medium mb-1">Max Tokens (output)</label><input id="set_max_tokens" type="number" min="1" step="1" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder=""/></div>
</div> </div>
<div class="flex flex-wrap items-center gap-2 pt-2"> <div class="flex flex-wrap items-center gap-2 pt-2">
<div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div> <div><input id="set_include_thoughts" type="checkbox" class="sr-only peer"><label for="set_include_thoughts" class="inline-flex cursor-pointer items-center rounded-full border border-slate-300 bg-transparent py-0.5 px-3 text-xs text-slate-500 peer-checked:border-gray-300 peer-checked:bg-gray-200 peer-checked:text-slate-800">Include thoughts</label></div>
@@ -68,10 +68,11 @@
</div> </div>
</div> </div>
</div> </div>
<div id="panelPrompt" class="p-4 space-y-4 hidden"> <div id="panelPrompt" class="p-4 space-y-4 overflow-y-auto 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 overflow-y-auto 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>
<div class="pt-0"> <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" autocorrect="off" autocapitalize="off" autocomplete="off"></pre> <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" autocorrect="off" autocapitalize="off" autocomplete="off"></pre>
@@ -87,20 +88,35 @@
</div> </div>
</div> </div>
</div> </div>
<dialog id="modelHelp" onclick="event.target===this&&this.close()" class="w-[calc(100%-2rem)] max-w-sm max-h-[85dvh] p-0 rounded-2xl border border-gray-200 bg-white text-gray-900 shadow-xl backdrop:bg-black/30">
<div class="px-4 py-3 border-b text-sm font-semibold flex items-center justify-between"><span>Model name</span><button type="button" onclick="modelHelp.close()" 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>
<div class="p-4 space-y-3 text-sm text-gray-700 [&_code]:text-xs [&_code]:bg-gray-100 [&_code]:rounded [&_code]:px-1 [&_code]:py-0.5 [&_a]:underline [&_a]:text-gray-900">
<p>The <b>Provider</b> selected in <b>Account Settings → General</b> is used, unless the model name starts with one of these prefixes:</p>
<div class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1"><code>or:</code><span>OpenRouter</span><code>oai:</code><span>OpenAI</span><code>g:</code><span>Google</span><code>cla:</code><span>Claude</span></div>
<p>After the prefix, write the model slug exactly the way that provider's API expects it:</p>
<div class="space-y-2">
<div><div class="font-medium">OpenRouter <span class="font-normal text-gray-400">· vendor/model</span></div><code>or:anthropic/claude-opus-5.5</code><div class="text-xs text-gray-500 mt-0.5">Browse slugs at <a href="https://openrouter.ai/models" target="_blank" rel="noopener">openrouter.ai/models</a></div></div>
<div><div class="font-medium">OpenAI</div><code>oai:gpt-6-sol</code><div class="text-xs text-gray-500 mt-0.5"><a href="https://platform.openai.com/docs/models" target="_blank" rel="noopener">OpenAI models</a></div></div>
<div><div class="font-medium">Google</div><code>g:gemini-3.8-flash</code><div class="text-xs text-gray-500 mt-0.5"><a href="https://ai.google.dev/gemini-api/docs/models" target="_blank" rel="noopener">Gemini models</a></div></div>
<div><div class="font-medium">Claude</div><code>cla:claude-opus-5-5</code><div class="text-xs text-gray-500 mt-0.5"><a href="https://docs.anthropic.com/en/docs/about-claude/models" target="_blank" rel="noopener">Claude models</a></div></div>
</div>
<p class="text-xs text-gray-500">Tip: add <code>:online</code> to the end to enable web search, e.g. <code>g:gemini-3.8-flash:online</code>.</p>
</div>
</dialog>
<div id="accountSettingsModal" class="hidden fixed inset-0 z-50"> <div id="accountSettingsModal" class="hidden fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/30"></div> <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="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="flex flex-col max-h-[calc(100dvh-5rem)] 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> <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"> <form id="accountSettingsForm" class="flex flex-col min-h-0 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><button type="button" id="accountTabUser" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">User</button></div> <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><button type="button" id="accountTabUser" class="flex-1 py-2 px-3 text-center border-b-2 border-transparent hover:border-gray-300">User</button></div>
<div id="accountPanelGeneral" class="p-4 space-y-4"> <div id="accountPanelGeneral" class="p-4 space-y-4 overflow-y-auto">
<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><option value="google">Google</option><option value="claude">Claude</option></select><p class="mt-1 text-xs text-gray-500">Or you can prefix model names with or:, oai:, g:, or cla: to override.</p></div> <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><option value="google">Google</option><option value="claude">Claude</option></select><p class="mt-1 text-xs text-gray-500">Or you can prefix model names with or:, oai:, g:, or cla: 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">Master Prompt</label><textarea id="set_master_prompt" rows="6" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="Applies to all sunes on this device"></textarea><p class="mt-1 text-xs text-gray-500">Stored locally.</p></div>
<div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div> <div><label class="block text-gray-700 font-medium mb-1">Model preference for titles</label><input id="set_title_model" type="text" class="w-full rounded-xl border border-gray-300 px-3 py-2" placeholder="or:google/gemma-3-12b-it"/><p class="mt-1 text-xs text-gray-500">Used for auto-generating thread titles.</p></div>
</div> </div>
<div id="accountPanelAPI" class="p-4 hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Custom Key 1</label><div class="relative"><input id="set_api_key_custom1" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_custom1" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.customKey1</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div></div></div> <div id="accountPanelAPI" class="p-4 overflow-y-auto hidden"><div class="grid grid-cols-2 gap-x-4 gap-y-4"><div><label class="block text-gray-700 font-medium mb-1">OpenRouter Key</label><div class="relative"><input id="set_api_key_or" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-or-..."><button type="button" data-reveal-for="set_api_key_or" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenRouter</code></p></div><div><label class="block text-gray-700 font-medium mb-1">OpenAI Key</label><div class="relative"><input id="set_api_key_oai" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-..."><button type="button" data-reveal-for="set_api_key_oai" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyOpenAI</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Google Key</label><div class="relative"><input id="set_api_key_g" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="AIza..."><button type="button" data-reveal-for="set_api_key_g" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Gemini/Studio. Use: <code>USER.apiKeyGoogle</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Claude Key</label><div class="relative"><input id="set_api_key_claude" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="sk-ant-..."><button type="button" data-reveal-for="set_api_key_claude" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.apiKeyClaude</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Cloudflare Token</label><div class="relative"><input id="set_api_key_cf" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_cf" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Not used. Use: <code>USER.apiKeyCloudflare</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Custom Key 1</label><div class="relative"><input id="set_api_key_custom1" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="..."><button type="button" data-reveal-for="set_api_key_custom1" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.customKey1</code></p></div><div><label class="block text-gray-700 font-medium mb-1">Github Token</label><div class="relative"><input id="set_gh_token" type="password" class="w-full rounded-xl border border-gray-300 px-3 py-2 pr-10" placeholder="ghp_..."><button type="button" data-reveal-for="set_gh_token" class="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"><i data-lucide="eye" class="h-4 w-4"></i></button></div><p class="mt-1 text-xs text-gray-500">Use: <code>USER.githubToken</code></p></div></div></div>
<div id="accountPanelUser" class="p-4 space-y-4 hidden"> <div id="accountPanelUser" class="p-4 space-y-4 overflow-y-auto hidden">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div> <div class="relative"><img id="userAvatarPreview" class="h-16 w-16 rounded-full object-cover bg-gray-200"><button type="button" id="setUserAvatarBtn" class="absolute bottom-0 right-0 h-6 w-6 rounded-full bg-white border border-gray-300 flex items-center justify-center hover:bg-gray-100" aria-label="Edit photo"><i data-lucide="edit-3" class="h-3 w-3"></i></button></div>
<input id="userAvatarInput" type="file" accept="image/*" class="hidden"> <input id="userAvatarInput" type="file" accept="image/*" class="hidden">
+2 -2
View File
@@ -1,7 +1,7 @@
<header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200"> <header id="topbar" class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
<div class="mx-auto w-full max-w-none px-4 py-3 grid grid-cols-3 items-center"> <div class="mx-auto w-full max-w-none px-4 py-2 grid grid-cols-3 items-center">
<button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button> <button id="sidebarBtnLeft" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Sunes" @click="document.getElementById('sidebarLeft').classList.remove('-translate-x-full');document.getElementById('sidebarOverlayLeft').classList.remove('hidden')"><i data-lucide="panel-left" class="h-5 w-5"></i></button>
<button id="suneBtnTop" class="justify-self-center h-8 w-8 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings">✺</button> <button id="suneBtnTop" class="justify-self-center h-10 w-10 rounded-full bg-gray-200 text-gray-900 flex items-center justify-center hover:bg-gray-300 active:scale-[.99] transition" title="Sune settings">✺</button>
<div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div> <div class="justify-self-end"><button id="sidebarBtnRight" class="h-8 w-8 rounded-xl bg-gray-100 hover:bg-gray-200 active:scale-[.99] transition flex items-center justify-center" title="Threads" @click="renderThreads();document.getElementById('sidebarRight').classList.remove('translate-x-full');document.getElementById('sidebarOverlayRight').classList.remove('hidden')"><i data-lucide="panel-right" class="h-5 w-5"></i></button></div>
</div> </div>
</header> </header>
+5 -5
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:)/,'').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});
} }
@@ -36,17 +37,16 @@ export const buildBody=()=>{
} }
} }
const b=payloadWithSampling({model:SUNE.model.replace(/^(or:|oai:|g:|cla:|cf:)/,''),messages:msgs,stream:true}); const b=payloadWithSampling({model:SUNE.model.replace(/^(or:|oai:|g:|cla:)/,''),messages:msgs,stream:true});
b.reasoning={...(SUNE.reasoning_effort&&SUNE.reasoning_effort!=='default'?{effort:SUNE.reasoning_effort}:{}),exclude:!SUNE.include_thoughts}; b.reasoning={...(SUNE.reasoning_effort&&SUNE.reasoning_effort!=='default'?{effort:SUNE.reasoning_effort}:{}),exclude:!SUNE.include_thoughts};
if(SUNE.verbosity)b.verbosity=SUNE.verbosity;
if(SUNE.img_output){b.modalities=['image'];b.image_config={aspect_ratio:SUNE.aspect_ratio||'1:1',image_size:SUNE.image_size||'1K'}} if(SUNE.img_output){b.modalities=['image'];b.image_config={aspect_ratio:SUNE.aspect_ratio||'1:1',image_size:SUNE.image_size||'1K'}}
return b return b
} }
async function streamORP(body,onDelta,streamId){ async function streamORP(body,onDelta,streamId){
const {USER,SUNE,state,gid,cacheStore}=window; const {USER,SUNE,state,gid,cacheStore}=window;
const model=SUNE.model,provider=model.startsWith('oai:')?'openai':model.startsWith('g:')?'google':model.startsWith('cla:')?'claude':model.startsWith('cf:')?'cloudflare':model.startsWith('or:')?'openrouter':USER.provider; const model=SUNE.model,provider=model.startsWith('oai:')?'openai':model.startsWith('g:')?'google':model.startsWith('cla:')?'claude':model.startsWith('or:')?'openrouter':USER.provider;
const apiKey=provider==='openai'?USER.apiKeyOpenAI:provider==='google'?USER.apiKeyGoogle:provider==='claude'?USER.apiKeyClaude:provider==='cloudflare'?USER.apiKeyCloudflare:USER.apiKeyOpenRouter; const apiKey=provider==='openai'?USER.apiKeyOpenAI:provider==='google'?USER.apiKeyGoogle:provider==='claude'?USER.apiKeyClaude:USER.apiKeyOpenRouter;
if(!apiKey){onDelta(window.localDemoReply(),true);return {ok:true,rid:streamId||null}} if(!apiKey){onDelta(window.localDemoReply(),true);return {ok:true,rid:streamId||null}}
const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null}; const r={rid:streamId||gid(),seq:-1,done:false,signaled:false,ws:null};
await cacheStore.setItem(r.rid,'busy'); await cacheStore.setItem(r.rid,'busy');
+1 -1
View File
@@ -28,7 +28,7 @@ export const generateTitleWithAI = async messages => {
{ role: 'system', content: sysPrompt }, { role: 'system', content: sysPrompt },
{ role: 'user', content: `${prePrompt}\n\n${convo}\n\n${postPrompt}` } { role: 'user', content: `${prePrompt}\n\n${convo}\n\n${postPrompt}` }
], ],
max_tokens: 6, max_tokens: 7,
temperature: 0.35 temperature: 0.35
}) })
}); });
+1 -2
View File
@@ -18,14 +18,13 @@ export const USER = {
set apiKeyCloudflare(v) { localStorage.setItem('cloudflare_api_key', v || ''); }, set apiKeyCloudflare(v) { localStorage.setItem('cloudflare_api_key', v || ''); },
get apiKey() { get apiKey() {
const p = this.provider; const p = this.provider;
return p === 'openai' ? this.apiKeyOpenAI : p === 'google' ? this.apiKeyGoogle : p === 'claude' ? this.apiKeyClaude : p === 'cloudflare' ? this.apiKeyCloudflare : this.apiKeyOpenRouter; return p === 'openai' ? this.apiKeyOpenAI : p === 'google' ? this.apiKeyGoogle : p === 'claude' ? this.apiKeyClaude : this.apiKeyOpenRouter;
}, },
set apiKey(v) { set apiKey(v) {
const p = this.provider; const p = this.provider;
if (p === 'openai') this.apiKeyOpenAI = v; if (p === 'openai') this.apiKeyOpenAI = v;
else if (p === 'google') this.apiKeyGoogle = v; else if (p === 'google') this.apiKeyGoogle = v;
else if (p === 'claude') this.apiKeyClaude = v; else if (p === 'claude') this.apiKeyClaude = v;
else if (p === 'cloudflare') this.apiKeyCloudflare = v;
else this.apiKeyOpenRouter = v; else this.apiKeyOpenRouter = v;
}, },
get masterPrompt() { return localStorage.getItem('master_prompt') || 'Always respond using markdown.'; }, get masterPrompt() { return localStorage.getItem('master_prompt') || 'Always respond using markdown.'; },
+1 -1
View File
@@ -16,7 +16,7 @@ export const fmtSize = b => {
return (x >= 10 ? Math.round(x) : Math.round(x * 10) / 10) + ' ' + u[i]; return (x >= 10 ? Math.round(x) : Math.round(x * 10) / 10) + ' ' + u[i];
}; };
export const asDataURL = f => new Promise(r => { const fr = new FileReader(); fr.onload = () => r(String(fr.result || '')); fr.readAsDataURL(f); }); export const asDataURL = f => new Promise(r => { const fr = new FileReader(); fr.onload = () => r(String(fr.result || '')); fr.readAsDataURL(f); });
export const imgToWebp = (f, D = 128, q = 80) => new Promise((r, j) => { export const imgToWebp = (f, D = 96, q = 75) => new Promise((r, j) => {
if (!f) return j(); if (!f) return j();
const i = new Image; const i = new Image;
i.onload = () => { i.onload = () => {
+4 -4
View File
@@ -9,18 +9,18 @@ export default defineConfig({
VitePWA({ VitePWA({
registerType:'autoUpdate', registerType:'autoUpdate',
manifest:{ manifest:{
id:'https://sune.planetrenox.com/', id:'/',
name:'Sune', name:'Sune',
short_name:'Sune', short_name:'Sune',
description:'OpenRouter GUI Frontend', description:'OpenRouter GUI Frontend',
start_url:'https://sune.planetrenox.com/', start_url:'/',
display:'standalone', display:'standalone',
orientation:'portrait', orientation:'portrait',
theme_color:'#FFFFFF', theme_color:'#FFFFFF',
background_color:'#000000', background_color:'#000000',
categories:['productivity','utilities'], categories:['productivity','utilities'],
icons:[{ src:'https://sune.planetrenox.com/appstore_content/✺.png', sizes:'1024x1024', type:'image/png' }], icons:[{ src:'/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' }] screenshots:[{ src:'/appstore_content/screenshot1.jpg', sizes:'1344x2693', type:'image/jpeg' },{ src:'/appstore_content/screenshot2.jpg', sizes:'1344x2699', type:'image/jpeg' }]
} }
}) })
] ]