mirror of
https://github.com/elder-plinius/P4RS3LT0NGV3.git
synced 2026-07-12 15:36:32 +02:00
added themes, updated title and meta for seo and bump to v4.0
This commit is contained in:
+19
-4
@@ -13,6 +13,7 @@ P4RS3LT0NGV3/
|
||||
├── index.template.html # HTML shell; tool *script* tags updated by inject-tool-scripts
|
||||
├── css/
|
||||
│ ├── style.css
|
||||
│ ├── themes-atmosphere.css # Optional per-theme atmosphere / component overrides
|
||||
│ └── notification.css
|
||||
├── js/
|
||||
│ ├── app.js # Vue app entry
|
||||
@@ -37,7 +38,8 @@ P4RS3LT0NGV3/
|
||||
│ │ ├── glitchTokens.js
|
||||
│ │ ├── history.js
|
||||
│ │ ├── notifications.js
|
||||
│ │ └── theme.js
|
||||
│ │ ├── openrouterModels.js # Fetch/cache OpenRouter model lists for Settings UI
|
||||
│ │ └── theme.js # Theme registry + applyTheme / cycleTheme
|
||||
│ └── tools/ # One *Tool.js per tab (extends Tool.js)
|
||||
│ ├── Tool.js
|
||||
│ ├── AntiClassifierTool.js
|
||||
@@ -98,7 +100,8 @@ P4RS3LT0NGV3/
|
||||
├── docs/
|
||||
│ ├── TOOL-SYSTEM.md
|
||||
│ ├── TOOL_ARCHITECTURE.md
|
||||
│ └── UI-COMPONENTS.md
|
||||
│ ├── UI-COMPONENTS.md
|
||||
│ └── THEMES.md # Adding / editing themes
|
||||
├── README.md
|
||||
└── CONTRIBUTING.md
|
||||
|
||||
@@ -118,7 +121,7 @@ dist/ # npm run build — gitignored
|
||||
|
||||
- **`js/core/`** — Shared business logic and infrastructure (not tab-specific)
|
||||
- Examples: `decoder.js` (DecodeTool, decoder pipeline), `steganography.js` (EmojiTool, steg engine), `toolRegistry.js` (registers tools, merges Vue surface), `transformOptions.js` (shared transform UI helpers)
|
||||
- **`js/utils/`** — Cross-cutting helpers (`clipboard`, `EmojiUtils` in `emoji.js`, notifications, theme, etc.)
|
||||
- **`js/utils/`** — Cross-cutting helpers (`clipboard`, `EmojiUtils` in `emoji.js`, notifications, `theme.js`, `openrouterModels.js`, etc.)
|
||||
- **`js/data/`** — Committed static payloads (models, prompts, glitch token data, end sequences, `emojiCompatibility.js`). **`emojiData.js`** is **not** edited here — it is **generated** to `dist/js/data/emojiData.js` by `npm run build:emoji`.
|
||||
- **`src/`** — `emojiWordMap.js` feeds the emoji build; `transformers/` holds transformer modules
|
||||
- **Generated bundle** — `npm run build:transforms` writes `dist/js/bundles/transforms-bundle.js` (a legacy `js/bundles/transforms-bundle.js` path may exist for older workflows and is gitignored)
|
||||
@@ -313,7 +316,18 @@ Utilities are shared helper functions used across the app. Currently, utility fu
|
||||
- Document with JSDoc comments
|
||||
- Consider adding to existing modules if functionality is related
|
||||
|
||||
**Note:** Prefer `js/utils/` for shared helpers (clipboard, emoji, escapeParser, focus, glitchTokens, history, notifications, theme). Use `js/config/` for constants.
|
||||
**Note:** Prefer `js/utils/` for shared helpers (clipboard, emoji, escapeParser, focus, glitchTokens, history, notifications, theme, openrouterModels). Use `js/config/` for constants.
|
||||
|
||||
### 4. Adding or Updating a Theme
|
||||
|
||||
Themes are registry entries plus CSS token blocks—not Vue components.
|
||||
|
||||
1. Register the theme in `js/utils/theme.js` (`themes` array).
|
||||
2. Add a `body.theme-<id> { … }` token block in `css/style.css` (copy from `body.theme-dark` or an existing custom theme).
|
||||
3. Optionally add atmosphere rules in `css/themes-atmosphere.css`.
|
||||
4. `npm run build`, then verify in the browser (dropdown, **`D`** cycle, nav, utility dock, toggles, mobile header utility button).
|
||||
|
||||
Full step-by-step guide, token checklist, and testing list: **[docs/THEMES.md](docs/THEMES.md)**.
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
@@ -417,6 +431,7 @@ This:
|
||||
- **Tool system**: `docs/TOOL-SYSTEM.md` — Templates, injection, UI vocabulary
|
||||
- **Tool architecture**: `docs/TOOL_ARCHITECTURE.md`
|
||||
- **UI components**: `docs/UI-COMPONENTS.md`
|
||||
- **Themes**: `docs/THEMES.md` — Adding and editing themes
|
||||
|
||||
## ✅ Checklist Before Submitting
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# 🐍 P4RS3LT0NGV3 - Universal Text Translator
|
||||
# 🐍 P4RS3LT0NGV3 4.0 — Universal Text Translator
|
||||
|
||||
A powerful web-based text transformation and steganography tool with **223** built-in text transforms spanning encodings, classical and modern ciphers, Unicode styles, formatting, and niche alphabets. Think of it as a universal translator for ALL alphabets and writing systems!
|
||||
A powerful web-based text transformation and steganography tool with **223** built-in text transforms spanning encodings, classical and modern ciphers, Unicode styles, formatting, and niche alphabets. Think of it as a universal translator for ALL alphabets and writing systems!
|
||||
|
||||
**Version 4.0** brings a redesigned desktop app shell, seven themes (including WCAG 2.1 AA **Accessible**), mobile utility panels, OpenRouter model curation, and responsive UI polish across all tools.
|
||||
|
||||
The app is a **static site**: run **`npm run build`** (after `npm install`), then open **`dist/index.html`** in your browser—no local server required. **Alternatively**, you can run it as a local app over HTTP with **`npm start`** or **`npx serve dist -l 8080`** (see [Getting Started](#getting-started) below). Core transforms, decoder, and steganography work **without** calling the cloud; features that use [OpenRouter](https://openrouter.ai/) need **network access** and an API key (see below).
|
||||
|
||||
@@ -286,7 +288,7 @@ Tabs appear in **UI order** below. **OpenRouter** (optional or required per tool
|
||||
- **Real-time**: Updates as you type.
|
||||
- **Script & language hints**: Unicode script ranges and Latin word-marker heuristics for common languages.
|
||||
- **AI translate to English** (optional, OpenRouter): When text looks foreign, optional one-shot translate to English.
|
||||
- **Keyboard shortcut**: **D**.
|
||||
- **Deep link**: `#decoder` in the URL opens this tab directly.
|
||||
|
||||
### 😀 **Emoji** (Steganography)
|
||||
|
||||
@@ -340,6 +342,18 @@ Tabs appear in **UI order** below. **OpenRouter** (optional or required per tool
|
||||
- **Dictionary mode**: Seeded random gibberish over a configurable character set.
|
||||
- **Removal mode**: Random or **specific** letter removal with batch **variations** and min/max strip lengths.
|
||||
|
||||
### 📷 **Codes** (QR & barcodes)
|
||||
|
||||
*Tab id: `codes` — deep link: `#codes` (generate) or `#codes/decode` (scan).*
|
||||
|
||||
- **Generate**: QR codes and common 1D barcodes from any text or numeric payload.
|
||||
- **Formats**: **QR Code** (PNG download), **Code 128**, **EAN-13**, and **Code 39** (SVG download).
|
||||
- **QR options**: Pixel size, quiet-zone margin, and error-correction level (L / M / Q / H).
|
||||
- **Barcode options**: Bar height, module width, and optional human-readable label under the bars.
|
||||
- **Decode**: Upload a PNG, JPEG, GIF, or WebP image to read QR codes and barcodes — **fully client-side** (image never leaves the browser).
|
||||
- **Copy & reuse**: Send decoded text to the Generate tab with one click.
|
||||
- **Offline-friendly**: QR/barcode libraries (`qrcode`, JsBarcode, ZXing) are bundled into `js/vendor/` at build time — no runtime CDN for this tab.
|
||||
|
||||
### 🪄 **PromptCraft** (via OpenRouter)
|
||||
|
||||
- **9 Mutation Strategies**: Rephrase, Obfuscate, Role-Play Wrap, Multi-Language, Expand, Compress, Metaphor, Fragment, and Custom
|
||||
@@ -355,13 +369,18 @@ models
|
||||
- **Same key**: Uses the same OpenRouter API key as Translation and PromptCraft.
|
||||
|
||||
### 📱 **User Experience**
|
||||
- **Dark/Light Theme**: Toggle between themes
|
||||
- **Themes**: **Advanced Settings** → Theme (Dark, Light, **Accessible**, BT6, Pliny, Cyberpunk, Wild West) — press **`D`** to cycle; choice saved in the browser
|
||||
- **Desktop layout**: Left tool nav, main workspace, right utility dock (Copy History, Glitch Tokens, End Sequences, Advanced Settings)
|
||||
- **Mobile / narrow screens**: Tool picker dropdown; utility panels slide in from the right (open via the **columns** icon in the header)
|
||||
- **Copy History**: Track all copied content with timestamps
|
||||
- **Auto-copy**: Automatically copy transformed text
|
||||
- **Keyboard Shortcuts**: Quick access to features
|
||||
- **Auto-copy**: Automatically copy transformed text (where enabled per tool)
|
||||
- **Keyboard Shortcuts**: Quick access to features (including **`D`** for theme cycle)
|
||||
- **Responsive Design**: Works on all device sizes
|
||||
- **Accessibility**: Screen reader friendly with proper ARIA labels
|
||||
- **Side panels**: Glitch token browser (optional data), end-sequence / delimiter strings for research, and **Advanced Settings** (OpenRouter key, steganography tuning)
|
||||
- **Side panels**: Glitch token browser (optional data), end-sequence / delimiter strings for research, and **Advanced Settings** (OpenRouter API key, model curation, steganography tuning)
|
||||
- **Deep links**: Open a specific tool tab via URL hash — e.g. `#decoder`, `#steganography`, `#codes`, `#codes/decode` (browser back/forward supported)
|
||||
|
||||
Contributors: see **[docs/THEMES.md](docs/THEMES.md)** for how to add or edit themes.
|
||||
|
||||
### 🔑 **OpenRouter API Key Setup**
|
||||
|
||||
@@ -369,9 +388,10 @@ models
|
||||
|
||||
1. Create an account at [openrouter.ai](https://openrouter.ai/)
|
||||
2. Generate an API key (starts with `sk-or-...`)
|
||||
3. In P4RS3LT0NGV3, click the **sliders icon** (top-right) to open **Advanced Settings**
|
||||
3. In P4RS3LT0NGV3, open **Advanced Settings** from the utility dock (desktop) or the **columns** icon in the header (mobile)
|
||||
4. Paste your key and click **Save Key**
|
||||
5. Your key is stored locally in your browser only — never sent anywhere except OpenRouter
|
||||
5. Choose which OpenRouter models appear in Translation, PromptCraft, Anti-Classifier, and related dropdowns
|
||||
6. Your key is stored locally in your browser only — never sent anywhere except OpenRouter
|
||||
|
||||
> **Tip:** Some models (like Gemma 3) are free on OpenRouter. Frontier models (Claude, GPT, Gemini Pro) require credits.
|
||||
|
||||
@@ -411,11 +431,12 @@ Notes:
|
||||
npm install
|
||||
|
||||
# Build all assets (required before use). Order matches package.json:
|
||||
# build:tools → build:copy → build:index → build:transforms → build:emoji → build:templates
|
||||
# build:tools → build:codes-vendor → build:copy → build:index → build:transforms → build:emoji → build:templates
|
||||
npm run build
|
||||
|
||||
# Or build individual components:
|
||||
npm run build:tools # Auto-discover tools, inject script tags into dist/index.html
|
||||
npm run build:codes-vendor # Bundle qrcode, JsBarcode, ZXing → dist/js/vendor/
|
||||
npm run build:copy # Copy static files to dist/
|
||||
npm run build:index # Generate src/transformers/index.js (ES module index)
|
||||
npm run build:transforms # Bundle all transformers to dist/js/bundles/transforms-bundle.js
|
||||
@@ -457,6 +478,7 @@ npm run preview # npm run build, then serve dist/
|
||||
- Transformers are bundled from `src/transformers/` to `dist/js/bundles/transforms-bundle.js`
|
||||
- Tool templates are injected from `templates/` into `dist/index.html`
|
||||
- Emoji data is generated to `dist/js/data/`
|
||||
- QR/barcode vendors (`qrcode`, `JsBarcode`, `@zxing/library`) are bundled to `dist/js/vendor/` via `npm run build:codes-vendor`
|
||||
|
||||
### **Browser Support**
|
||||
- Chrome/Edge 80+
|
||||
@@ -469,6 +491,15 @@ npm run preview # npm run build, then serve dist/
|
||||
- **Memory Efficient**: Streams large text without loading into memory
|
||||
- **Optimized Rendering**: Efficient DOM updates with Vue.js
|
||||
|
||||
## 🔧 **What's New in 4.0**
|
||||
|
||||
- 🆕 **Version 4.0 release**: Major UI refresh — desktop three-column layout, theme system, and mobile utility dock
|
||||
- 🆕 **Theme system**: Seven themes (Dark, Light, **Accessible**, BT6, Pliny, Cyberpunk, Wild West) with token-based CSS and atmosphere layers
|
||||
- 🆕 **Desktop app shell**: Left tool nav, main workspace, right utility dock (Copy History, Glitch Tokens, End Sequences, Settings)
|
||||
- 🆕 **Mobile utility panels**: Slide-over panels with responsive tab bar (no horizontal scroll)
|
||||
- 🆕 **OpenRouter model settings**: Curate which models appear in AI tool dropdowns (Settings)
|
||||
- 🆕 **Responsive tool polish**: Bijection, Tokenade, transform cards, and topbar layout improvements
|
||||
|
||||
## 🔧 **Recent Fixes & Improvements**
|
||||
|
||||
### **Fixed Issues**
|
||||
@@ -478,6 +509,8 @@ npm run preview # npm run build, then serve dist/
|
||||
- ✅ **Reverse Functions**: Added missing reverse functions for many transforms
|
||||
|
||||
### **New Features**
|
||||
- 🆕 **Codes tool**: Generate QR codes and barcodes (Code 128, EAN-13, Code 39); decode from uploaded images client-side
|
||||
- 🆕 **URL deep links**: Jump to any tool tab with `#tab` hashes (e.g. `#codes/decode`)
|
||||
- 🆕 **AI Translation**: Translate to 20+ languages (including dead/exotic) via OpenRouter using TranslateGemma prompt format
|
||||
- 🆕 **PromptCraft Tool**: AI-powered prompt mutation with 9 strategies and 48+ models
|
||||
- 🆕 **OpenRouter Integration**: Unified API key management for all AI-powered features
|
||||
@@ -541,4 +574,4 @@ This project is open source. See LICENSE file for details.
|
||||
|
||||
---
|
||||
|
||||
**P4RS3LT0NGV3** - Because sometimes you need to speak in tongues that don't exist! 🐉✨
|
||||
**P4RS3LT0NGV3 4.0** - Because sometimes you need to speak in tongues that don't exist! 🐉✨
|
||||
|
||||
@@ -57,19 +57,20 @@ let indexContent = fs.readFileSync(templatePath, 'utf8');
|
||||
indexContent = indexContent.replace(/\r\n/g, '\n');
|
||||
|
||||
// Find the tool-content-container
|
||||
const startMarker = '<div id="tool-content-container">';
|
||||
const endMarker = '</div>\n\n </div>\n\n <!-- Copy History Panel -->';
|
||||
const startMarker = '<div id="tool-content-container"';
|
||||
const endMarker = '</div>\n </main>';
|
||||
|
||||
const startIndex = indexContent.indexOf(startMarker);
|
||||
const endIndex = indexContent.indexOf(endMarker);
|
||||
const startTagEnd = indexContent.indexOf('>', startIndex);
|
||||
const endIndex = indexContent.indexOf(endMarker, startTagEnd);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1) {
|
||||
if (startIndex === -1 || startTagEnd === -1 || endIndex === -1) {
|
||||
console.error('\n❌ Could not find tool content container markers');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build the replacement content
|
||||
const before = indexContent.substring(0, startIndex + startMarker.length);
|
||||
const before = indexContent.substring(0, startTagEnd + 1);
|
||||
const after = indexContent.substring(endIndex);
|
||||
|
||||
const replacement = `
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background-color: #25282c;
|
||||
color: #00FF41;
|
||||
background-color: var(--secondary-bg);
|
||||
color: var(--success-color);
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--input-border);
|
||||
box-shadow: var(--panel-shadow-soft);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -16,7 +17,7 @@
|
||||
}
|
||||
|
||||
.copy-notification.error {
|
||||
color: #ff4141;
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.copy-notification.fade-out {
|
||||
|
||||
+2089
-534
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+179
@@ -0,0 +1,179 @@
|
||||
# Themes
|
||||
|
||||
P4RS3LT0NGV3 uses a **token-based theme system**. Most UI reads CSS custom properties (`--accent-color`, `--button-bg`, etc.) from `body.theme-*` blocks, so new themes are mostly palette + typography work—not per-component rewrites.
|
||||
|
||||
## Built-in themes
|
||||
|
||||
| ID | Name | Notes |
|
||||
|----|------|--------|
|
||||
| `dark` | Dark | Default; blue accent |
|
||||
| `light` | Light | Light surfaces; blue accent |
|
||||
| `accessible` | Accessible | WCAG 2.1 AA high-contrast; system fonts; strong focus rings |
|
||||
| `bt6` | BT6 | Black + blood red + gold; mono titles |
|
||||
| `pliny` | Pliny | CRT green-on-black |
|
||||
| `cyberpunk` | Cyberpunk | Magenta + cyan neon |
|
||||
| `wildwest` | Wild West | Cream / sage / dusty rose (light `color-scheme`) |
|
||||
|
||||
Users pick a theme in **Advanced Settings** (utility dock → Settings tab). Press **`D`** to cycle themes. The choice is saved in `localStorage` under the key `theme`.
|
||||
|
||||
## Accessible theme
|
||||
|
||||
The **`accessible`** theme targets **[WCAG 2.1 Level AA](https://www.w3.org/TR/WCAG21/)** for color, contrast, focus, and motion.
|
||||
|
||||
| Requirement | Implementation |
|
||||
|-------------|------------------|
|
||||
| **1.4.3 Contrast (AA)** | `#000000` / `#595959` text on `#ffffff`; `#004080` accent; category active colors darkened for white labels |
|
||||
| **1.4.11 Non-text contrast** | `2px` borders at `#555555` on inputs, buttons, and cards |
|
||||
| **2.4.7 Focus visible** | `3px` `#004080` outline + `2px` offset on all interactive elements |
|
||||
| **1.4.12 Text spacing** | `1rem` base size, `1.5` line-height, system UI fonts |
|
||||
| **1.4.1 Use of color** | Active nav/tabs use underline + weight + border, not color alone |
|
||||
| **2.3.3 Animation** | Logo glitch and decorative card effects disabled |
|
||||
| **Transform cards** | Text **Options** / **Favorite** buttons; fixed preview contrast and row heights |
|
||||
| **Translation picker** | Text **Favorite** (and **Remove** on custom langs); star/× icons hidden |
|
||||
|
||||
Decorative themes (Cyberpunk, Pliny, etc.) are unchanged. For audits, test with the **Accessible** theme selected and keyboard-only navigation.
|
||||
|
||||
## How it works
|
||||
|
||||
1. **`js/utils/theme.js`** — Registry (`themes` array), `applyTheme()`, `cycleTheme()`, persistence.
|
||||
2. **`css/style.css`** — One `body.theme-<id> { … }` block per theme defining design tokens.
|
||||
3. **`css/themes-atmosphere.css`** — Optional “premium” overrides for custom themes (nav, transform cards, backgrounds, fonts). Imported at the top of `style.css`.
|
||||
4. **`index.template.html`** — Inline bootstrap script applies the saved theme **before** Vue mounts (avoids flash of wrong theme).
|
||||
|
||||
When a theme is applied, the body gets:
|
||||
|
||||
- `theme-<id>` — primary selector (e.g. `theme-bt6`)
|
||||
- `dark-theme` or `light-theme` — legacy aliases still used by a few rules
|
||||
|
||||
Copy an existing `body.theme-*` block as a starting point—`body.theme-dark` and `body.theme-light` are the simplest references; custom themes often follow the BT6 / Pliny pattern.
|
||||
|
||||
## Adding a new theme
|
||||
|
||||
### 1. Register in `js/utils/theme.js`
|
||||
|
||||
```javascript
|
||||
{ id: 'mytheme', name: 'My Theme', icon: 'fa-star' }
|
||||
```
|
||||
|
||||
- **`id`** — lowercase, no spaces; becomes `body.theme-mytheme`.
|
||||
- **`name`** — label in the dropdown.
|
||||
- **`icon`** — optional Font Awesome class (not shown in dropdown today, but kept for future use).
|
||||
|
||||
### 2. Add tokens in `css/style.css`
|
||||
|
||||
Add a new block (copy from `body.theme-dark` or an existing custom theme):
|
||||
|
||||
```css
|
||||
body.theme-mytheme {
|
||||
--main-bg-color: #…;
|
||||
--secondary-bg: #…;
|
||||
--nav-bg: #…;
|
||||
--utility-tab-bg: #…;
|
||||
--text-color: #…;
|
||||
--text-muted: #…;
|
||||
--accent-color: #…;
|
||||
--accent-color-rgb: R, G, B; /* comma-separated, no spaces */
|
||||
--accent-hover: #…;
|
||||
/* … accent-tint-*, surface-*, button-*, input-*, error-*, etc. */
|
||||
--glitch-color: #…;
|
||||
--glitch-color-rgb: R, G, B;
|
||||
--switch-border: #…;
|
||||
--switch-surface-glow: rgba(…);
|
||||
--switch-track-bg: #…;
|
||||
--switch-track-border: #…;
|
||||
--switch-track-checked-bg: #…;
|
||||
--switch-track-checked-glow: rgba(…);
|
||||
--switch-thumb-bg: #…;
|
||||
--switch-thumb-glow: rgba(…);
|
||||
--switch-thumb-checked-bg: #…;
|
||||
--switch-thumb-checked-glow: rgba(…);
|
||||
color-scheme: dark; /* or light for pale themes */
|
||||
}
|
||||
```
|
||||
|
||||
**Required tokens** — At minimum, match what `body.theme-dark` defines: surfaces, text, accent (+ `-rgb`), buttons, inputs, focus/error/success, tooltips, switch tokens, and legacy aliases (`--text-primary`, `--border-color`, etc.) at the bottom of the block.
|
||||
|
||||
**Light themes** — Set `color-scheme: light` and ensure `--text-on-accent` contrasts on `--accent-color`. Wild West is the reference for a light custom theme.
|
||||
|
||||
**Optional theme tokens** (used by atmosphere CSS):
|
||||
|
||||
- `--theme-display-font`, `--theme-ui-font`, `--theme-mono-font`
|
||||
- `--theme-secondary`, `--theme-secondary-rgb`
|
||||
- `--theme-radius` — corner radius (`0` for sharp, `2px`–`6px` for rounded)
|
||||
- Wild West also uses `--theme-cream`, `--theme-charcoal`, etc.
|
||||
|
||||
### 3. Optional atmosphere in `css/themes-atmosphere.css`
|
||||
|
||||
For a distinctive look beyond color swaps, add a section:
|
||||
|
||||
```css
|
||||
body.theme-mytheme .app-root::before { /* background texture / tint */ }
|
||||
body.theme-mytheme .transform-button { /* card chrome */ }
|
||||
body.theme-mytheme .app-nav .tab-buttons button.active { /* nav active state */ }
|
||||
```
|
||||
|
||||
If you add a custom theme here, also extend the shared selectors at the top of the file (`body[class*="theme-…"]`) so shell layering (`::before` / z-index) stays consistent.
|
||||
|
||||
**Style guidelines** (project convention):
|
||||
|
||||
- Use **flat colors** only—no CSS gradients unless explicitly requested.
|
||||
- Toggle switches (`.switch.neon`) pick up colors from `--switch-*` tokens automatically.
|
||||
- Avoid logo `::before` / `::after` decorations.
|
||||
|
||||
### 4. Fonts (if needed)
|
||||
|
||||
Google Fonts are imported at the top of `css/style.css`. Add families there, then reference them in your `--theme-*-font` tokens.
|
||||
|
||||
### 5. Select carets & dropdowns
|
||||
|
||||
Custom themes with non-default accents often need a `select` caret override. See existing blocks:
|
||||
|
||||
```css
|
||||
body.theme-mytheme select:not(.settings-theme-select) { background-image: url("data:…"); }
|
||||
body.theme-mytheme .settings-theme-select { … }
|
||||
```
|
||||
|
||||
Wild West required extra care so theme dropdown carets do not tile—use `background-repeat: no-repeat` and longhand `background-*` when overriding selects.
|
||||
|
||||
### 6. Build & test
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
|
||||
Open `dist/index.html` (or `npm start`) and check:
|
||||
|
||||
- [ ] Advanced Settings theme dropdown lists your theme
|
||||
- [ ] **`D`** cycles through it without errors
|
||||
- [ ] Nav, main content, utility dock (desktop)
|
||||
- [ ] Mobile: utility panel toggle in header; no overlapping FAB
|
||||
- [ ] Transform grid, toggles, inputs, notifications
|
||||
- [ ] Advanced Settings / OpenRouter model dropdown (if applicable)
|
||||
- [ ] Reload persists choice
|
||||
|
||||
No change to `index.template.html` is required unless you rename the bootstrap script flow—the dropdown is driven by `themeOptions` from Vue, which reads `ThemeUtils.getThemes()`.
|
||||
|
||||
## File checklist
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `js/utils/theme.js` | Add `{ id, name, icon }` |
|
||||
| `css/style.css` | Add `body.theme-<id> { … }` token block |
|
||||
| `css/themes-atmosphere.css` | Optional component/atmosphere overrides |
|
||||
| `css/style.css` `@import` | Already imports `themes-atmosphere.css` — no change |
|
||||
| `README.md` | Add theme to user-facing list (optional) |
|
||||
|
||||
## Updating an existing theme
|
||||
|
||||
1. Edit the `body.theme-<id>` token block in `css/style.css`.
|
||||
2. Adjust matching rules in `css/themes-atmosphere.css` if the theme has premium styling.
|
||||
3. Rebuild and spot-check the same checklist above.
|
||||
|
||||
Token changes propagate everywhere that uses `var(--…)`; you rarely need to touch individual tools or templates.
|
||||
|
||||
## Related docs
|
||||
|
||||
- **User-facing overview**: `README.md` → User Experience
|
||||
- **Project layout**: `CONTRIBUTING.md` → Project Structure
|
||||
- **UI patterns**: `docs/UI-COMPONENTS.md`
|
||||
+356
-273
@@ -3,7 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Parseltongue 2.0 - LLM Payload Crafter</title>
|
||||
<title>Parseltongue 4.0 — Text Encoder, Decoder & Steganography Tool</title>
|
||||
<meta name="description" content="Free online text encoder & decoder with 223+ transforms: ciphers, Base64, Unicode styles, emoji steganography & QR codes. No install required — because sometimes you need to speak in tongues that don't exist.">
|
||||
<meta name="keywords" content="P4RS3LT0NGV3, Parseltongue, text encoder, text decoder, cipher tool, steganography, Unicode converter, Base64">
|
||||
<meta name="application-name" content="P4RS3LT0NGV3">
|
||||
<meta property="og:title" content="Parseltongue 4.0 — Text Encoder, Decoder & Steganography Tool">
|
||||
<meta property="og:description" content="223+ encodings, ciphers & steganography in your browser. Because sometimes you need to speak in tongues that don't exist.">
|
||||
<meta property="og:type" content="website">
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/notification.css">
|
||||
@@ -13,161 +19,185 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="container">
|
||||
<header>
|
||||
<div class="logo">
|
||||
<h1>🐉️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️︎︎️️️️︎︎︎️️️️️︎︎︎️︎︎︎️️️︎️︎︎️️️️︎️︎︎︎️︎︎︎️︎︎️️︎️︎️︎︎️️️️︎️︎︎︎️︎︎︎️︎︎︎️︎️︎︎️️️︎️︎︎️︎︎︎️︎️︎️︎︎️️️︎︎️︎︎︎︎︎️︎️︎︎︎︎️︎︎️︎︎️️︎︎︎️︎︎︎️︎️︎️︎︎︎️︎︎︎︎️︎️️️︎︎︎️︎️️️︎︎︎️︎️️️︎︎️︎︎️️︎︎︎️︎︎️️️️︎️︎️︎️️︎︎️︎︎︎️︎️︎︎️️️︎︎️︎️︎️︎︎︎︎︎︎️︎︎️️︎︎︎️︎︎️︎︎️︎️︎︎️️️︎︎️︎️️︎︎️︎️️️️️︎︎︎︎️️️️️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎︎︎️︎️️︎️️︎︎︎︎︎︎︎️︎︎︎️︎ P4RS3LT0NGV3</h1>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var saved = localStorage.getItem('theme') || 'dark';
|
||||
document.body.classList.add('theme-' + saved);
|
||||
document.body.classList.add(
|
||||
saved === 'light' || saved === 'accessible' ? 'light-theme' : 'dark-theme'
|
||||
);
|
||||
} catch (e) {
|
||||
document.body.classList.add('theme-dark', 'dark-theme');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<div id="app" class="app-root">
|
||||
<header class="app-topbar">
|
||||
<div class="logo app-logo">
|
||||
<h1>🐉 P4RS3LT0NGV3</h1>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="actions topbar-actions">
|
||||
<button
|
||||
@click="toggleCopyHistory"
|
||||
class="history-button"
|
||||
title="Show copy history"
|
||||
aria-label="Show copy history"
|
||||
type="button"
|
||||
class="topbar-utility-btn"
|
||||
@click="mobileUtilityOpen ? closeMobileUtility() : openMobileUtility()"
|
||||
:title="mobileUtilityOpen ? 'Close panels' : 'Open panels'"
|
||||
:aria-label="mobileUtilityOpen ? 'Close panels' : 'Open panels'"
|
||||
:aria-expanded="mobileUtilityOpen"
|
||||
>
|
||||
<i class="fas fa-history"></i>
|
||||
<i :class="mobileUtilityOpen ? 'fas fa-xmark' : 'fas fa-columns'"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="toggleTheme"
|
||||
@keyup.d="toggleTheme"
|
||||
class="theme-button"
|
||||
title="Toggle dark mode (D)"
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
<i class="fas" :class="isDarkTheme ? 'fa-moon' : 'fa-sun'"></i>
|
||||
</button>
|
||||
<a
|
||||
href="https://github.com/elder-plinius/P4RS3LT0NGV3"
|
||||
target="_blank"
|
||||
class="github-button"
|
||||
<a
|
||||
href="https://github.com/elder-plinius/P4RS3LT0NGV3"
|
||||
target="_blank"
|
||||
class="github-button"
|
||||
title="View source on GitHub"
|
||||
aria-label="View source code on GitHub"
|
||||
>
|
||||
<i class="fab fa-github"></i>
|
||||
</a>
|
||||
<button
|
||||
@click="toggleUnicodePanel"
|
||||
class="history-button"
|
||||
title="Advanced Settings"
|
||||
aria-label="Advanced Settings"
|
||||
>
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="toggleGlitchTokenPanel"
|
||||
class="history-button"
|
||||
title="Glitch Tokens"
|
||||
aria-label="Glitch Tokens"
|
||||
>
|
||||
<i class="fas fa-bug"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="toggleEndSequencePanel"
|
||||
class="history-button"
|
||||
title="End sequences (delimiter strings)"
|
||||
aria-label="End sequences"
|
||||
>
|
||||
<i class="fas fa-stop"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tabs">
|
||||
<div class="tab-buttons" role="tablist" aria-label="Tools">
|
||||
<!-- Dynamically generated tab buttons from tool registry -->
|
||||
<button
|
||||
v-for="tool in registeredTools"
|
||||
v-if="!tool.hidden"
|
||||
:key="tool.id"
|
||||
:class="{ active: activeTab === tool.id }"
|
||||
@click="switchToTab(tool.id)"
|
||||
:title="tool.title"
|
||||
role="tab"
|
||||
:aria-selected="activeTab === tool.id"
|
||||
>
|
||||
<i :class="'fas ' + tool.icon"></i> {{ tool.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-tool-select">
|
||||
<label for="mobile-tool-select">Selected Tool</label>
|
||||
<select
|
||||
id="mobile-tool-select"
|
||||
class="mobile-tool-dropdown"
|
||||
:value="activeTab"
|
||||
aria-label="Selected tool"
|
||||
@change="switchToTab($event.target.value)"
|
||||
>
|
||||
<template v-for="tool in registeredTools">
|
||||
<option
|
||||
v-if="!tool.hidden"
|
||||
:key="tool.id"
|
||||
:value="tool.id"
|
||||
>{{ tool.name }}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="tool-content-container">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Copy History Panel -->
|
||||
<div class="app-sidebar copy-history-panel" :class="{ 'active': showCopyHistory }">
|
||||
<div class="app-sidebar-header copy-history-header">
|
||||
<h3><i class="fas fa-history"></i> Copy History</h3>
|
||||
<div class="header-actions">
|
||||
<button
|
||||
v-if="copyHistory.length > 0"
|
||||
@click.stop="clearCopyHistory"
|
||||
class="clear-history-button"
|
||||
title="Clear all history"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<button class="close-button" @click="toggleCopyHistory" title="Close history">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="app-shell">
|
||||
<nav class="app-nav" role="tablist" aria-label="Tools">
|
||||
<div class="tab-buttons">
|
||||
<button
|
||||
v-for="tool in registeredTools"
|
||||
v-if="!tool.hidden"
|
||||
:key="tool.id"
|
||||
:class="{ active: activeTab === tool.id }"
|
||||
@click="switchToTab(tool.id)"
|
||||
:title="tool.title"
|
||||
role="tab"
|
||||
:aria-selected="activeTab === tool.id"
|
||||
>
|
||||
<i :class="'fas ' + tool.icon"></i>
|
||||
<span class="nav-label">{{ tool.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="app-sidebar-body copy-history-content">
|
||||
<div v-if="copyHistory.length === 0" class="no-history">
|
||||
<p>No copy history yet. Use the app features to auto-copy content.</p>
|
||||
</div>
|
||||
<div v-else class="history-items">
|
||||
<div v-for="(item, index) in copyHistory" :key="item.id || index" class="history-item">
|
||||
<div class="history-item-header">
|
||||
<span class="history-source">{{ item.source }}</span>
|
||||
<span class="history-time">{{ formatHistoryTime(item.timestamp) }}</span>
|
||||
</nav>
|
||||
|
||||
<main class="app-main">
|
||||
<div class="tab-tool-select">
|
||||
<label for="mobile-tool-select">Selected Tool</label>
|
||||
<select
|
||||
id="mobile-tool-select"
|
||||
class="mobile-tool-dropdown"
|
||||
:value="activeTab"
|
||||
aria-label="Selected tool"
|
||||
@change="switchToTab($event.target.value)"
|
||||
>
|
||||
<template v-for="tool in registeredTools">
|
||||
<option
|
||||
v-if="!tool.hidden"
|
||||
:key="tool.id"
|
||||
:value="tool.id"
|
||||
>{{ tool.name }}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="tool-content-container" class="tool-content-container">
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div
|
||||
v-if="mobileUtilityOpen"
|
||||
class="utility-backdrop"
|
||||
@click="closeMobileUtility"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<aside
|
||||
class="app-utility"
|
||||
:class="{ 'utility-open': mobileUtilityOpen }"
|
||||
aria-label="Utilities"
|
||||
@click.stop
|
||||
>
|
||||
<div class="utility-tab-bar" role="tablist" aria-label="Utility panels" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="utility-tab-btn"
|
||||
:class="{ active: activeUtilityPanel === 'history' }"
|
||||
@click.stop="switchUtilityPanel('history')"
|
||||
role="tab"
|
||||
:aria-selected="activeUtilityPanel === 'history'"
|
||||
>
|
||||
Copy History
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="utility-tab-btn"
|
||||
:class="{ active: activeUtilityPanel === 'glitch' }"
|
||||
@click.stop="switchUtilityPanel('glitch')"
|
||||
role="tab"
|
||||
:aria-selected="activeUtilityPanel === 'glitch'"
|
||||
>
|
||||
Glitch Tokens
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="utility-tab-btn"
|
||||
:class="{ active: activeUtilityPanel === 'endsequences' }"
|
||||
@click.stop="switchUtilityPanel('endsequences')"
|
||||
role="tab"
|
||||
:aria-selected="activeUtilityPanel === 'endsequences'"
|
||||
>
|
||||
End Sequences
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="utility-tab-btn"
|
||||
:class="{ active: activeUtilityPanel === 'settings' }"
|
||||
@click.stop="switchUtilityPanel('settings')"
|
||||
role="tab"
|
||||
:aria-selected="activeUtilityPanel === 'settings'"
|
||||
>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="utility-panels">
|
||||
<div v-show="activeUtilityPanel === 'history'" class="utility-panel copy-history-panel">
|
||||
<div class="utility-panel-toolbar" v-if="copyHistory.length > 0">
|
||||
<button
|
||||
@click.stop="clearCopyHistory"
|
||||
class="clear-history-button"
|
||||
title="Clear all history"
|
||||
>
|
||||
<i class="fas fa-trash"></i> Clear all
|
||||
</button>
|
||||
</div>
|
||||
<div class="utility-panel-body copy-history-content">
|
||||
<div v-if="copyHistory.length === 0" class="no-history">
|
||||
<p>No copy history yet. Use the app features to auto-copy content.</p>
|
||||
</div>
|
||||
<div class="history-content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<div class="history-actions">
|
||||
<button class="copy-again-button" @click="copyToClipboard(item.content)" title="Copy again">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button class="remove-history-button" @click.stop="removeFromCopyHistory(item.id)" title="Remove from history">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div v-else class="history-items">
|
||||
<div v-for="(item, index) in copyHistory" :key="item.id || index" class="history-item">
|
||||
<div class="history-item-header">
|
||||
<span class="history-source">{{ item.source }}</span>
|
||||
<span class="history-time">{{ formatHistoryTime(item.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="history-content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<div class="history-actions">
|
||||
<button class="copy-again-button" @click="copyToClipboard(item.content)" title="Copy again">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button class="remove-history-button" @click.stop="removeFromCopyHistory(item.id)" title="Remove from history">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Glitch Tokens Panel -->
|
||||
<div class="app-sidebar glitch-token-panel" :class="{ 'active': showGlitchTokenPanel }">
|
||||
<div class="app-sidebar-header glitch-token-header">
|
||||
<h3><i class="fas fa-bug"></i> Glitch Tokens</h3>
|
||||
<div class="header-actions">
|
||||
<button class="close-button" @click="toggleGlitchTokenPanel" title="Close glitch tokens">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-sidebar-body glitch-token-content">
|
||||
<div v-show="activeUtilityPanel === 'glitch'" class="utility-panel glitch-token-panel">
|
||||
<div class="utility-panel-body glitch-token-content">
|
||||
<!-- Filter Section -->
|
||||
<div class="glitch-token-filters">
|
||||
<div class="filter-group">
|
||||
@@ -251,147 +281,199 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- End sequences sidebar (delimiter / stop strings for research) -->
|
||||
<div class="app-sidebar end-sequence-panel" :class="{ active: showEndSequencePanel }">
|
||||
<div class="app-sidebar-header end-sequence-header">
|
||||
<h3><i class="fas fa-stop"></i> End sequences</h3>
|
||||
<div class="header-actions">
|
||||
<button class="close-button" type="button" @click="toggleEndSequencePanel" title="Close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-sidebar-body end-sequence-content">
|
||||
<p class="end-sequence-lede">
|
||||
Strings sometimes used to probe delimiter and termination behavior. Copy into your payloads as needed for authorized testing.
|
||||
</p>
|
||||
<div
|
||||
v-for="cat in endSequenceCategories"
|
||||
:key="cat.title"
|
||||
class="endsequence-category"
|
||||
>
|
||||
<h4>{{ cat.title }}</h4>
|
||||
<div class="endsequence-items">
|
||||
<button
|
||||
v-for="(item, idx) in cat.items"
|
||||
:key="cat.title + '-' + idx"
|
||||
type="button"
|
||||
class="endsequence-item"
|
||||
@click="copyEndSequence(item.value)"
|
||||
:title="'Copy to clipboard'"
|
||||
:aria-label="'Copy ' + item.label"
|
||||
<div v-show="activeUtilityPanel === 'endsequences'" class="utility-panel end-sequence-panel">
|
||||
<div class="utility-panel-body end-sequence-content">
|
||||
<p class="end-sequence-lede">
|
||||
Strings sometimes used to probe delimiter and termination behavior. Copy into your payloads as needed for authorized testing.
|
||||
</p>
|
||||
<div
|
||||
v-for="cat in endSequenceCategories"
|
||||
:key="cat.title"
|
||||
class="endsequence-category"
|
||||
>
|
||||
<code class="endsequence-label">{{ item.label }}</code>
|
||||
<span class="endsequence-copy-affordance" aria-hidden="true">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
</button>
|
||||
<h4>{{ cat.title }}</h4>
|
||||
<div class="endsequence-items">
|
||||
<button
|
||||
v-for="(item, idx) in cat.items"
|
||||
:key="cat.title + '-' + idx"
|
||||
type="button"
|
||||
class="endsequence-item"
|
||||
@click="copyEndSequence(item.value)"
|
||||
:title="'Copy to clipboard'"
|
||||
:aria-label="'Copy ' + item.label"
|
||||
>
|
||||
<code class="endsequence-label">{{ item.label }}</code>
|
||||
<span class="endsequence-copy-affordance" aria-hidden="true">
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="activeUtilityPanel === 'settings'" id="unicode-options-panel" class="utility-panel unicode-options-panel">
|
||||
<div class="utility-panel-body unicode-panel-content">
|
||||
<div class="settings-section theme-settings-section">
|
||||
<h4><i class="fas fa-palette"></i> Theme</h4>
|
||||
<small class="settings-hint">App appearance. Press <kbd>D</kbd> to cycle themes.</small>
|
||||
<label class="settings-theme-field">
|
||||
Active theme
|
||||
<select
|
||||
class="settings-theme-select"
|
||||
:value="activeTheme"
|
||||
@change="setTheme($event.target.value)"
|
||||
aria-label="Select theme"
|
||||
>
|
||||
<option
|
||||
v-for="theme in themeOptions"
|
||||
:key="theme.id"
|
||||
:value="theme.id"
|
||||
>
|
||||
{{ theme.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<hr class="settings-divider" />
|
||||
<div class="settings-section api-key-section">
|
||||
<h4><i class="fas fa-key"></i> OpenRouter API Key</h4>
|
||||
<small class="settings-hint">Required for Translation, PromptCraft, and Anti-Classifier. Stored locally in your browser only.</small>
|
||||
<div class="api-key-input-row">
|
||||
<input
|
||||
:type="showApiKey ? 'text' : 'password'"
|
||||
v-model="openrouterApiKey"
|
||||
placeholder="sk-or-..."
|
||||
class="api-key-input"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<button class="api-key-toggle" @click="showApiKey = !showApiKey" :title="showApiKey ? 'Hide key' : 'Show key'">
|
||||
<i :class="showApiKey ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="api-key-actions">
|
||||
<button class="api-key-save" @click="saveApiKey" :disabled="!openrouterApiKey">
|
||||
<i class="fas fa-save"></i> Save Key
|
||||
</button>
|
||||
<button class="api-key-clear" @click="clearApiKey" v-if="openrouterApiKey">
|
||||
<i class="fas fa-trash"></i> Clear
|
||||
</button>
|
||||
<small v-if="apiKeySaved" class="apply-status">Saved</small>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="settings-divider" />
|
||||
<div class="settings-section openrouter-models-section">
|
||||
<h4><i class="fas fa-robot"></i> AI Models</h4>
|
||||
<small class="settings-hint">Choose which models appear in tool dropdowns. Disabled models stay hidden unless currently selected.</small>
|
||||
<div class="openrouter-models-toolbar">
|
||||
<input
|
||||
type="search"
|
||||
v-model="openRouterModelsFilterQuery"
|
||||
class="openrouter-models-search"
|
||||
placeholder="Search models…"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button type="button" class="openrouter-models-action" @click="enableAllOpenRouterModels">Show all</button>
|
||||
<button type="button" class="openrouter-models-action" @click="showFreeOpenRouterModelsOnly">Free only</button>
|
||||
<button type="button" class="openrouter-models-action" @click="refreshOpenRouterModels(true)" :disabled="openRouterModelsLoading">
|
||||
<i class="fas" :class="openRouterModelsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="openrouter-models-summary">
|
||||
{{ openRouterModels.length }} shown in dropdowns · {{ openRouterModelsCatalog.length }} loaded
|
||||
<span v-if="openRouterModelsDisabled.length"> · {{ openRouterModelsDisabled.length }} hidden</span>
|
||||
</small>
|
||||
<small v-if="openRouterModelsError" class="openrouter-model-hint openrouter-model-hint-error">{{ openRouterModelsError }}</small>
|
||||
<div class="openrouter-models-list" role="group" aria-label="Model visibility">
|
||||
<label
|
||||
v-for="model in filteredOpenRouterModelsCatalog"
|
||||
:key="model.id"
|
||||
class="openrouter-model-toggle-row"
|
||||
:class="{ 'is-virtual-router': model.virtual }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isOpenRouterModelEnabled(model.id)"
|
||||
@change="toggleOpenRouterModelEnabled(model.id)"
|
||||
/>
|
||||
<span class="openrouter-model-toggle-text">
|
||||
<span class="openrouter-model-toggle-name">{{ formatOpenRouterModelLabel(model) }}</span>
|
||||
<small v-if="model.summary" class="openrouter-model-toggle-summary">{{ model.summary }}</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="settings-divider" />
|
||||
<h4><i class="fas fa-user-secret"></i> Emoji Steganography</h4>
|
||||
<small class="settings-hint">Global encoding options for the Steganography tool (emoji carrier bit patterns). Not used by Unicode style transforms.</small>
|
||||
<div class="options-grid steg-adv-panel">
|
||||
<label>
|
||||
Initial Presentation
|
||||
<select class="steg-initial-presentation">
|
||||
<option value="emoji">Emoji (VS16)</option>
|
||||
<option value="text">Text (VS15)</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Bit-0 Selector
|
||||
<select class="steg-vs-zero">
|
||||
<option value="\ufe0e">VS15 (\ufe0e)</option>
|
||||
<option value="\ufe0f">VS16 (\ufe0f)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Bit-1 Selector
|
||||
<select class="steg-vs-one">
|
||||
<option value="\ufe0f">VS16 (\ufe0f)</option>
|
||||
<option value="\ufe0e">VS15 (\ufe0e)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Inter-bit Zero-Width
|
||||
<select class="steg-inter-zw">
|
||||
<option value="">None</option>
|
||||
<option value="\u200C">ZWNJ (\u200C)</option>
|
||||
<option value="\u200D">ZWJ (\u200D)</option>
|
||||
<option value="\u200B">ZWSP (\u200B)</option>
|
||||
<option value="\ufeff">BOM (\ufeff)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Inter-bit Every N bits
|
||||
<input class="steg-inter-every" type="number" min="1" max="8" value="1" />
|
||||
</label>
|
||||
<label>
|
||||
Bit Order
|
||||
<select class="steg-bit-order">
|
||||
<option value="msb">MSB First</option>
|
||||
<option value="lsb">LSB First</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Trailing Zero-Width
|
||||
<select class="steg-trailing-zw">
|
||||
<option value="\u200B">ZWSP (\u200B)</option>
|
||||
<option value="\u200C">ZWNJ (\u200C)</option>
|
||||
<option value="\u200D">ZWJ (\u200D)</option>
|
||||
<option value="\ufeff">BOM (\ufeff)</option>
|
||||
<option value="">None</option>
|
||||
</select>
|
||||
</label>
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<button class="apply-steg-options" :class="{ applied: unicodeApplyFlash }" :disabled="unicodeApplyBusy" @click="applyUnicodeOptions" title="These options affect Unicode-based steganography encoding/decoding.">Apply</button>
|
||||
<small v-if="unicodeApplyFlash" class="apply-status">Applied</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Settings Panel (inside app so Vue bindings work) -->
|
||||
<div id="unicode-options-panel" class="app-sidebar unicode-options-panel">
|
||||
<div class="app-sidebar-header unicode-panel-header">
|
||||
<h3><i class="fas fa-sliders-h"></i> Advanced Settings</h3>
|
||||
<button class="close-button" title="Close Advanced Settings"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="app-sidebar-body unicode-panel-content">
|
||||
<!-- OpenRouter API Key -->
|
||||
<div class="settings-section api-key-section">
|
||||
<h4><i class="fas fa-key"></i> OpenRouter API Key</h4>
|
||||
<small class="settings-hint">Required for Translation, PromptCraft, and Anti-Classifier. Stored locally in your browser only.</small>
|
||||
<div class="api-key-input-row">
|
||||
<input
|
||||
:type="showApiKey ? 'text' : 'password'"
|
||||
v-model="openrouterApiKey"
|
||||
placeholder="sk-or-..."
|
||||
class="api-key-input"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<button class="api-key-toggle" @click="showApiKey = !showApiKey" :title="showApiKey ? 'Hide key' : 'Show key'">
|
||||
<i :class="showApiKey ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="api-key-actions">
|
||||
<button class="api-key-save" @click="saveApiKey" :disabled="!openrouterApiKey">
|
||||
<i class="fas fa-save"></i> Save Key
|
||||
</button>
|
||||
<button class="api-key-clear" @click="clearApiKey" v-if="openrouterApiKey">
|
||||
<i class="fas fa-trash"></i> Clear
|
||||
</button>
|
||||
<small v-if="apiKeySaved" class="apply-status">Saved</small>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="settings-divider" />
|
||||
<!-- Steganography Options -->
|
||||
<h4><i class="fas fa-user-secret"></i> Steganography Options</h4>
|
||||
</div>
|
||||
<div class="app-sidebar-body unicode-panel-content options-grid steg-adv-panel">
|
||||
<label>
|
||||
Initial Presentation
|
||||
<select class="steg-initial-presentation">
|
||||
<option value="emoji">Emoji (VS16)</option>
|
||||
<option value="text">Text (VS15)</option>
|
||||
<option value="none">None</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Bit-0 Selector
|
||||
<select class="steg-vs-zero">
|
||||
<option value="\ufe0e">VS15 (\ufe0e)</option>
|
||||
<option value="\ufe0f">VS16 (\ufe0f)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Bit-1 Selector
|
||||
<select class="steg-vs-one">
|
||||
<option value="\ufe0f">VS16 (\ufe0f)</option>
|
||||
<option value="\ufe0e">VS15 (\ufe0e)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Inter-bit Zero-Width
|
||||
<select class="steg-inter-zw">
|
||||
<option value="">None</option>
|
||||
<option value="\u200C">ZWNJ (\u200C)</option>
|
||||
<option value="\u200D">ZWJ (\u200D)</option>
|
||||
<option value="\u200B">ZWSP (\u200B)</option>
|
||||
<option value="\ufeff">BOM (\ufeff)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Inter-bit Every N bits
|
||||
<input class="steg-inter-every" type="number" min="1" max="8" value="1" />
|
||||
</label>
|
||||
<label>
|
||||
Bit Order
|
||||
<select class="steg-bit-order">
|
||||
<option value="msb">MSB First</option>
|
||||
<option value="lsb">LSB First</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Trailing Zero-Width
|
||||
<select class="steg-trailing-zw">
|
||||
<option value="\u200B">ZWSP (\u200B)</option>
|
||||
<option value="\u200C">ZWNJ (\u200C)</option>
|
||||
<option value="\u200D">ZWJ (\u200D)</option>
|
||||
<option value="\ufeff">BOM (\ufeff)</option>
|
||||
<option value="">None</option>
|
||||
</select>
|
||||
</label>
|
||||
<div style="display:flex; align-items:center; gap:10px;">
|
||||
<button class="apply-steg-options" :class="{ applied: unicodeApplyFlash }" :disabled="unicodeApplyBusy" @click="applyUnicodeOptions" title="These options affect Unicode-based steganography encoding/decoding.">Apply</button>
|
||||
<small v-if="unicodeApplyFlash" class="apply-status">Applied</small>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -423,6 +505,7 @@
|
||||
<script src="js/utils/history.js"></script>
|
||||
<script src="js/utils/clipboard.js"></script>
|
||||
<script src="js/utils/theme.js"></script>
|
||||
<script src="js/utils/openrouterModels.js"></script>
|
||||
<script src="js/utils/routing.js"></script>
|
||||
<script src="js/utils/emoji.js"></script>
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
const baseData = {
|
||||
isDarkTheme: true,
|
||||
activeTheme: 'dark',
|
||||
themeOptions: (window.ThemeUtils && window.ThemeUtils.getThemes)
|
||||
? window.ThemeUtils.getThemes()
|
||||
: [{ id: 'dark', name: 'Dark' }, { id: 'light', name: 'Light' }],
|
||||
activeTab: 'transforms',
|
||||
registeredTools: [],
|
||||
universalDecodeInput: '',
|
||||
@@ -14,6 +17,9 @@ const baseData = {
|
||||
copyHistory: [],
|
||||
maxHistoryItems: window.CONFIG.MAX_HISTORY_ITEMS,
|
||||
showCopyHistory: false,
|
||||
activeUtilityPanel: 'history',
|
||||
mobileUtilityOpen: false,
|
||||
utilityPanelInteractionGuard: false,
|
||||
showUnicodePanel: false,
|
||||
unicodeApplyBusy: false,
|
||||
unicodeApplyFlash: false,
|
||||
@@ -33,7 +39,20 @@ const baseData = {
|
||||
allGlitchTokens: [],
|
||||
openrouterApiKey: localStorage.getItem('openrouter-api-key') || '',
|
||||
showApiKey: false,
|
||||
apiKeySaved: false
|
||||
apiKeySaved: false,
|
||||
openRouterModels: (window.OpenRouterModels && window.OpenRouterModels.getStaticFallback)
|
||||
? window.OpenRouterModels.getStaticFallback()
|
||||
: [],
|
||||
openRouterModelsCatalog: (window.OpenRouterModels && window.OpenRouterModels.getStaticFallback)
|
||||
? window.OpenRouterModels.getStaticFallback()
|
||||
: [],
|
||||
openRouterModelsDisabled: (window.OpenRouterModels && window.OpenRouterModels.loadDisabledIds)
|
||||
? window.OpenRouterModels.loadDisabledIds()
|
||||
: [],
|
||||
openRouterModelsFilterQuery: '',
|
||||
openRouterModelsLoading: false,
|
||||
openRouterModelsError: '',
|
||||
openRouterModelsKeyInfo: null
|
||||
};
|
||||
|
||||
const toolData = (window.toolRegistry && typeof window.toolRegistry.mergeVueData === 'function')
|
||||
@@ -45,21 +64,107 @@ const toolMethods = (window.toolRegistry && typeof window.toolRegistry.mergeVueM
|
||||
? window.toolRegistry.mergeVueMethods()
|
||||
: {};
|
||||
|
||||
Vue.component('openrouter-model-select', {
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
label: { type: String, default: 'Model' },
|
||||
showRefresh: { type: Boolean, default: true }
|
||||
},
|
||||
computed: {
|
||||
models: function() {
|
||||
return this.$root.openRouterModels || [];
|
||||
},
|
||||
loading: function() {
|
||||
return this.$root.openRouterModelsLoading;
|
||||
},
|
||||
error: function() {
|
||||
return this.$root.openRouterModelsError;
|
||||
},
|
||||
keyInfo: function() {
|
||||
return this.$root.openRouterModelsKeyInfo;
|
||||
},
|
||||
hasApiKey: function() {
|
||||
return this.$root.getOpenRouterApiKey ? !!this.$root.getOpenRouterApiKey() : false;
|
||||
},
|
||||
routerHint: function() {
|
||||
if (!this.value || !window.OpenRouterModels || !window.OpenRouterModels.getRouterHint) {
|
||||
return '';
|
||||
}
|
||||
return window.OpenRouterModels.getRouterHint(this.value);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange: function(event) {
|
||||
this.$emit('input', event.target.value);
|
||||
},
|
||||
refresh: function() {
|
||||
if (this.$root.refreshOpenRouterModels) {
|
||||
this.$root.refreshOpenRouterModels(true);
|
||||
}
|
||||
},
|
||||
formatLabel: function(model) {
|
||||
return this.$root.formatOpenRouterModelLabel
|
||||
? this.$root.formatOpenRouterModelLabel(model)
|
||||
: (model && model.name) || '';
|
||||
}
|
||||
},
|
||||
template:
|
||||
'<label class="openrouter-model-picker">' +
|
||||
'<span class="openrouter-model-label">{{ label }}</span>' +
|
||||
'<div class="openrouter-model-row">' +
|
||||
'<select ' +
|
||||
'class="openrouter-model-select" ' +
|
||||
':value="value" ' +
|
||||
'@change="onChange" ' +
|
||||
':disabled="loading && !models.length"' +
|
||||
'>' +
|
||||
'<option v-if="loading && !models.length" disabled value="">Loading models…</option>' +
|
||||
'<option v-for="m in models" :key="m.id" :value="m.id">{{ formatLabel(m) }}</option>' +
|
||||
'</select>' +
|
||||
'<button ' +
|
||||
'v-if="showRefresh" ' +
|
||||
'type="button" ' +
|
||||
'class="openrouter-model-refresh" ' +
|
||||
'@click="refresh" ' +
|
||||
':disabled="loading" ' +
|
||||
'title="Refresh model list from OpenRouter"' +
|
||||
'aria-label="Refresh model list"' +
|
||||
'>' +
|
||||
'<i class="fas" :class="loading ? \'fa-spinner fa-spin\' : \'fa-sync-alt\'"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<small v-if="error" class="openrouter-model-hint openrouter-model-hint-error">{{ error }}</small>' +
|
||||
'<small v-else-if="routerHint" class="openrouter-model-hint openrouter-model-hint-router">{{ routerHint }}</small>' +
|
||||
'<small v-else-if="!hasApiKey" class="openrouter-model-hint">Add an OpenRouter key in Settings to load models for your account.</small>' +
|
||||
'<small v-else-if="keyInfo && keyInfo.is_free_tier" class="openrouter-model-hint">Free tier account — models marked · free need no credits.</small>' +
|
||||
'<small v-else class="openrouter-model-hint">Curate visible models in Settings → AI Models.</small>' +
|
||||
'</label>'
|
||||
});
|
||||
|
||||
window.app = new Vue({
|
||||
el: '#app',
|
||||
data: mergedData,
|
||||
computed: {
|
||||
filteredOpenRouterModelsCatalog: function() {
|
||||
var query = (this.openRouterModelsFilterQuery || '').trim().toLowerCase();
|
||||
var catalog = this.openRouterModelsCatalog || [];
|
||||
if (!query) return catalog;
|
||||
return catalog.filter(function(model) {
|
||||
var haystack = [
|
||||
model.id,
|
||||
model.name,
|
||||
model.provider,
|
||||
model.summary
|
||||
].filter(Boolean).join(' ').toLowerCase();
|
||||
return haystack.indexOf(query) !== -1;
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: Object.assign({}, toolMethods || {}, {
|
||||
toggleUnicodePanel(event) {
|
||||
if (this.unicodePanelToggleLock) return;
|
||||
this.unicodePanelToggleLock = true;
|
||||
|
||||
this.showUnicodePanel = !this.showUnicodePanel;
|
||||
const panel = document.getElementById('unicode-options-panel');
|
||||
if (panel) {
|
||||
if (this.showUnicodePanel) panel.classList.add('active');
|
||||
else panel.classList.remove('active');
|
||||
}
|
||||
|
||||
this.switchUtilityPanel('settings');
|
||||
setTimeout(() => {
|
||||
this.unicodePanelToggleLock = false;
|
||||
}, 300);
|
||||
@@ -211,34 +316,74 @@ window.app = new Vue({
|
||||
}
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
this.isDarkTheme = window.ThemeUtils.toggleTheme(this.isDarkTheme);
|
||||
setTheme(themeId) {
|
||||
if (!window.ThemeUtils) return;
|
||||
this.activeTheme = window.ThemeUtils.applyTheme(themeId);
|
||||
},
|
||||
|
||||
cycleTheme() {
|
||||
if (!window.ThemeUtils) return;
|
||||
this.activeTheme = window.ThemeUtils.cycleTheme(this.activeTheme);
|
||||
},
|
||||
|
||||
toggleCopyHistory() {
|
||||
this.showCopyHistory = !this.showCopyHistory;
|
||||
|
||||
if (this.showCopyHistory && this.copyHistory.length > 0) {
|
||||
this.$nextTick(() => {
|
||||
const firstCopyButton = document.querySelector('.copy-again-button');
|
||||
this.switchUtilityPanel('history');
|
||||
},
|
||||
|
||||
toggleGlitchTokenPanel() {
|
||||
this.switchUtilityPanel('glitch');
|
||||
},
|
||||
|
||||
toggleEndSequencePanel() {
|
||||
this.switchUtilityPanel('endsequences');
|
||||
},
|
||||
|
||||
markUtilityPanelInteraction() {
|
||||
this.utilityPanelInteractionGuard = true;
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.utilityPanelInteractionGuard = false;
|
||||
}, 400);
|
||||
},
|
||||
|
||||
switchUtilityPanel(panelId) {
|
||||
var isMobile = window.matchMedia && window.matchMedia('(max-width: 768px)').matches;
|
||||
|
||||
this.activeUtilityPanel = panelId;
|
||||
this.showCopyHistory = panelId === 'history';
|
||||
this.showGlitchTokenPanel = panelId === 'glitch';
|
||||
this.showEndSequencePanel = panelId === 'endsequences';
|
||||
this.showUnicodePanel = panelId === 'settings';
|
||||
|
||||
if (isMobile) {
|
||||
this.mobileUtilityOpen = true;
|
||||
this.markUtilityPanelInteraction();
|
||||
}
|
||||
|
||||
if (panelId === 'glitch' && !this.glitchTokensLoaded) {
|
||||
this.loadGlitchTokens();
|
||||
}
|
||||
|
||||
if (panelId === 'history' && this.copyHistory.length > 0) {
|
||||
this.$nextTick(function() {
|
||||
var firstCopyButton = document.querySelector('.copy-again-button');
|
||||
if (firstCopyButton) {
|
||||
firstCopyButton.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleGlitchTokenPanel(event) {
|
||||
this.showGlitchTokenPanel = !this.showGlitchTokenPanel;
|
||||
|
||||
// Load tokens if not already loaded
|
||||
if (this.showGlitchTokenPanel && !this.glitchTokensLoaded) {
|
||||
this.loadGlitchTokens();
|
||||
|
||||
closeMobileUtility() {
|
||||
if (this.utilityPanelInteractionGuard) {
|
||||
return;
|
||||
}
|
||||
this.mobileUtilityOpen = false;
|
||||
},
|
||||
|
||||
toggleEndSequencePanel() {
|
||||
this.showEndSequencePanel = !this.showEndSequencePanel;
|
||||
openMobileUtility() {
|
||||
this.mobileUtilityOpen = true;
|
||||
this.markUtilityPanelInteraction();
|
||||
},
|
||||
|
||||
copyEndSequence(value) {
|
||||
@@ -397,6 +542,133 @@ window.app = new Vue({
|
||||
showCopiedPopup() {
|
||||
window.NotificationUtils.showCopiedPopup();
|
||||
},
|
||||
|
||||
getOpenRouterApiKey() {
|
||||
var key = (this.openrouterApiKey || '').trim();
|
||||
if (key) return key;
|
||||
if (window.OpenRouterModels && window.OpenRouterModels.getApiKey) {
|
||||
return window.OpenRouterModels.getApiKey();
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
formatOpenRouterModelLabel(model) {
|
||||
if (window.OpenRouterModels && window.OpenRouterModels.formatLabel) {
|
||||
return window.OpenRouterModels.formatLabel(model);
|
||||
}
|
||||
return model && model.name ? model.name : '';
|
||||
},
|
||||
|
||||
isOpenRouterModelEnabled(modelId) {
|
||||
if (!window.OpenRouterModels) return true;
|
||||
return window.OpenRouterModels.isModelEnabled(modelId, this.openRouterModelsDisabled);
|
||||
},
|
||||
|
||||
rebuildOpenRouterDropdown() {
|
||||
if (!window.OpenRouterModels) return;
|
||||
var pinned = window.OpenRouterModels.getPinnedModelIds(this);
|
||||
this.openRouterModels = window.OpenRouterModels.filterForDropdown(
|
||||
this.openRouterModelsCatalog,
|
||||
this.openRouterModelsDisabled,
|
||||
pinned
|
||||
);
|
||||
},
|
||||
|
||||
toggleOpenRouterModelEnabled(modelId) {
|
||||
if (!modelId) return;
|
||||
var disabled = this.openRouterModelsDisabled.slice();
|
||||
var index = disabled.indexOf(modelId);
|
||||
if (index === -1) {
|
||||
disabled.push(modelId);
|
||||
} else {
|
||||
disabled.splice(index, 1);
|
||||
}
|
||||
this.openRouterModelsDisabled = disabled;
|
||||
if (window.OpenRouterModels) {
|
||||
window.OpenRouterModels.saveDisabledIds(disabled);
|
||||
}
|
||||
this.rebuildOpenRouterDropdown();
|
||||
this.syncOpenRouterModelSelections();
|
||||
},
|
||||
|
||||
enableAllOpenRouterModels() {
|
||||
this.openRouterModelsDisabled = [];
|
||||
if (window.OpenRouterModels) {
|
||||
window.OpenRouterModels.saveDisabledIds([]);
|
||||
}
|
||||
this.rebuildOpenRouterDropdown();
|
||||
this.syncOpenRouterModelSelections();
|
||||
},
|
||||
|
||||
showFreeOpenRouterModelsOnly() {
|
||||
var disabled = (this.openRouterModelsCatalog || [])
|
||||
.filter(function(model) {
|
||||
return model && model.id && !model.free && !model.virtual;
|
||||
})
|
||||
.map(function(model) { return model.id; });
|
||||
this.openRouterModelsDisabled = disabled;
|
||||
if (window.OpenRouterModels) {
|
||||
window.OpenRouterModels.saveDisabledIds(disabled);
|
||||
}
|
||||
this.rebuildOpenRouterDropdown();
|
||||
this.syncOpenRouterModelSelections();
|
||||
},
|
||||
|
||||
syncOpenRouterModelSelections() {
|
||||
if (!window.OpenRouterModels || !this.openRouterModels.length) return;
|
||||
var models = this.openRouterModels;
|
||||
var ensure = window.OpenRouterModels.ensureValidSelection.bind(window.OpenRouterModels);
|
||||
|
||||
if (typeof this.pcModel !== 'undefined') {
|
||||
this.pcModel = ensure(this.pcModel, models, localStorage.getItem('pc-model') || 'openrouter/auto');
|
||||
}
|
||||
if (typeof this.acModel !== 'undefined') {
|
||||
this.acModel = ensure(this.acModel, models, localStorage.getItem('ac-model') || 'openrouter/auto');
|
||||
}
|
||||
if (typeof this.saModel !== 'undefined') {
|
||||
this.saModel = ensure(this.saModel, models, localStorage.getItem('sa-model') || 'openrouter/free');
|
||||
}
|
||||
if (typeof this.translateModel !== 'undefined') {
|
||||
this.translateModel = ensure(this.translateModel, models, localStorage.getItem('translate-model') || 'google/gemma-3-27b-it');
|
||||
}
|
||||
},
|
||||
|
||||
refreshOpenRouterModels: async function(force) {
|
||||
if (!window.OpenRouterModels) return;
|
||||
if (this.openRouterModelsLoading) return;
|
||||
|
||||
this.openRouterModelsLoading = true;
|
||||
this.openRouterModelsError = '';
|
||||
|
||||
var apiKey = this.getOpenRouterApiKey();
|
||||
|
||||
try {
|
||||
var models = await window.OpenRouterModels.fetch(apiKey, { force: !!force });
|
||||
this.openRouterModelsCatalog = models;
|
||||
this.rebuildOpenRouterDropdown();
|
||||
this.syncOpenRouterModelSelections();
|
||||
|
||||
if (apiKey) {
|
||||
this.openRouterModelsKeyInfo = await window.OpenRouterModels.fetchKeyInfo(apiKey);
|
||||
} else {
|
||||
this.openRouterModelsKeyInfo = null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('OpenRouter model fetch failed:', err);
|
||||
var fallback = window.OpenRouterModels.getStaticFallback();
|
||||
this.openRouterModelsCatalog = window.OpenRouterModels.mergeWithVirtual(fallback);
|
||||
this.rebuildOpenRouterDropdown();
|
||||
this.syncOpenRouterModelSelections();
|
||||
|
||||
if (err && err.status === 401) {
|
||||
this.openRouterModelsError = 'Invalid API key — check Settings.';
|
||||
} else {
|
||||
this.openRouterModelsError = (err && err.message) || 'Could not load models; using offline list.';
|
||||
}
|
||||
} finally {
|
||||
this.openRouterModelsLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
saveApiKey() {
|
||||
var trimmed = (this.openrouterApiKey || '').trim();
|
||||
@@ -406,6 +678,7 @@ window.app = new Vue({
|
||||
this.apiKeySaved = true;
|
||||
this.showNotification('API key saved', 'success');
|
||||
setTimeout(() => { this.apiKeySaved = false; }, 2000);
|
||||
this.refreshOpenRouterModels(true);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -416,6 +689,8 @@ window.app = new Vue({
|
||||
localStorage.removeItem('openrouter_api_key');
|
||||
localStorage.removeItem('plinyos-api-key');
|
||||
this.showNotification('API key cleared', 'success');
|
||||
this.openRouterModelsKeyInfo = null;
|
||||
this.refreshOpenRouterModels(true);
|
||||
},
|
||||
|
||||
setupPasteHandlers() {
|
||||
@@ -431,15 +706,19 @@ window.app = new Vue({
|
||||
}
|
||||
}),
|
||||
mounted() {
|
||||
if (window.ThemeUtils && window.ThemeUtils.initializeTheme) {
|
||||
this.isDarkTheme = window.ThemeUtils.initializeTheme();
|
||||
if (this.isDarkTheme) {
|
||||
document.body.classList.add('dark-theme');
|
||||
} else {
|
||||
document.body.classList.add('light-theme');
|
||||
}
|
||||
} else if (this.isDarkTheme) {
|
||||
document.body.classList.add('dark-theme');
|
||||
if (window.ThemeUtils) {
|
||||
this.activeTheme = window.ThemeUtils.applyTheme(window.ThemeUtils.initializeTheme());
|
||||
this._themeKeyHandler = (event) => {
|
||||
if (event.defaultPrevented || event.repeat) return;
|
||||
if (event.key !== 'd' && event.key !== 'D') return;
|
||||
var tag = event.target && event.target.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (event.target && event.target.isContentEditable)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.cycleTheme();
|
||||
};
|
||||
document.addEventListener('keydown', this._themeKeyHandler);
|
||||
}
|
||||
|
||||
if (window.toolRegistry && typeof window.toolRegistry.mergeVueLifecycle === 'function') {
|
||||
@@ -453,6 +732,8 @@ window.app = new Vue({
|
||||
this.registeredTools = window.toolRegistry.getAll();
|
||||
}
|
||||
|
||||
this.refreshOpenRouterModels(false);
|
||||
|
||||
var initialRoute = window.TabRouting && window.TabRouting.parse();
|
||||
if (initialRoute && initialRoute.tab && this.getValidToolIds().includes(initialRoute.tab)) {
|
||||
this.applyRouteFromHash();
|
||||
@@ -468,20 +749,6 @@ window.app = new Vue({
|
||||
};
|
||||
window.addEventListener('hashchange', this._onHashChange);
|
||||
|
||||
this.$nextTick(() => {
|
||||
const closeButton = document.querySelector('#unicode-options-panel .close-button');
|
||||
if (closeButton) {
|
||||
const handleClose = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.toggleUnicodePanel(e);
|
||||
};
|
||||
|
||||
closeButton.addEventListener('click', handleClose, { passive: false });
|
||||
closeButton.addEventListener('touchend', handleClose, { passive: false });
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.custom-tooltip')) {
|
||||
return;
|
||||
@@ -583,6 +850,11 @@ window.app = new Vue({
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (this._themeKeyHandler) {
|
||||
document.removeEventListener('keydown', this._themeKeyHandler);
|
||||
this._themeKeyHandler = null;
|
||||
}
|
||||
|
||||
if (this._onHashChange) {
|
||||
window.removeEventListener('hashchange', this._onHashChange);
|
||||
this._onHashChange = null;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Application Configuration Constants
|
||||
*/
|
||||
window.CONFIG = {
|
||||
APP_VERSION: '4.0.0',
|
||||
|
||||
// History configuration
|
||||
MAX_HISTORY_ITEMS: 50,
|
||||
|
||||
|
||||
+14
-46
@@ -1,51 +1,19 @@
|
||||
/**
|
||||
* Shared OpenRouter model list for PromptCraft, Anti-Classifier, etc.
|
||||
* Loaded before tool scripts.
|
||||
* Offline fallback when OpenRouter model API is unreachable.
|
||||
* Live lists are loaded via OpenRouterModels.fetch() using the user's API key.
|
||||
*/
|
||||
window.OPENROUTER_MODELS = [
|
||||
{ id: 'anthropic/claude-opus-4.6', name: 'Claude Opus 4.6', provider: 'Anthropic' },
|
||||
{ id: 'anthropic/claude-sonnet-4.6', name: 'Claude Sonnet 4.6', provider: 'Anthropic' },
|
||||
{ id: 'anthropic/claude-sonnet-4.5', name: 'Claude Sonnet 4.5', provider: 'Anthropic' },
|
||||
{ id: 'anthropic/claude-opus-4', name: 'Claude Opus 4', provider: 'Anthropic' },
|
||||
{ id: 'anthropic/claude-sonnet-4', name: 'Claude Sonnet 4', provider: 'Anthropic' },
|
||||
{ id: 'openai/gpt-5.4', name: 'GPT-5.4', provider: 'OpenAI' },
|
||||
{ id: 'openai/gpt-5.4-pro', name: 'GPT-5.4 Pro', provider: 'OpenAI' },
|
||||
{ id: 'openai/gpt-4.1', name: 'GPT-4.1', provider: 'OpenAI' },
|
||||
{ id: 'google/gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', provider: 'Google' },
|
||||
{ id: 'google/gemini-2.5-pro-preview', name: 'Gemini 2.5 Pro', provider: 'Google' },
|
||||
{ id: 'x-ai/grok-4.20-beta', name: 'Grok 4.20 Beta', provider: 'xAI' },
|
||||
{ id: 'x-ai/grok-4', name: 'Grok 4', provider: 'xAI' },
|
||||
{ id: 'deepseek/deepseek-v3.2', name: 'DeepSeek V3.2', provider: 'DeepSeek' },
|
||||
{ id: 'mistralai/mistral-large-3-2512', name: 'Mistral Large 3', provider: 'Mistral' },
|
||||
{ id: 'openai/o3-pro', name: 'o3-pro', provider: 'OpenAI' },
|
||||
{ id: 'openai/o3', name: 'o3', provider: 'OpenAI' },
|
||||
{ id: 'openai/o4-mini', name: 'o4-mini', provider: 'OpenAI' },
|
||||
{ id: 'deepseek/deepseek-r1-0528', name: 'DeepSeek R1 (0528)', provider: 'DeepSeek' },
|
||||
{ id: 'deepseek/deepseek-r1', name: 'DeepSeek R1', provider: 'DeepSeek' },
|
||||
{ id: 'qwen/qwq-32b', name: 'QwQ 32B', provider: 'Qwen' },
|
||||
{ id: 'anthropic/claude-haiku-4.5', name: 'Claude Haiku 4.5', provider: 'Anthropic' },
|
||||
{ id: 'openai/gpt-5.4-mini', name: 'GPT-5.4 Mini', provider: 'OpenAI' },
|
||||
{ id: 'openai/gpt-4.1-mini', name: 'GPT-4.1 Mini', provider: 'OpenAI' },
|
||||
{ id: 'openai/gpt-4.1-nano', name: 'GPT-4.1 Nano', provider: 'OpenAI' },
|
||||
{ id: 'google/gemini-3-flash-preview', name: 'Gemini 3 Flash', provider: 'Google' },
|
||||
{ id: 'google/gemini-2.5-flash-preview', name: 'Gemini 2.5 Flash', provider: 'Google' },
|
||||
{ id: 'google/gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash Lite', provider: 'Google' },
|
||||
{ id: 'x-ai/grok-4.1-fast', name: 'Grok 4.1 Fast', provider: 'xAI' },
|
||||
window.OPENROUTER_MODELS_FALLBACK = [
|
||||
{ id: 'openrouter/free', name: 'Free router', summary: 'Zero cost — random free model', provider: 'OpenRouter', virtual: true },
|
||||
{ id: 'openrouter/auto', name: 'Auto router', summary: 'Smart routing — billed per model', provider: 'OpenRouter', virtual: true },
|
||||
{ id: 'google/gemma-3-27b-it', name: 'Gemma 3 27B', provider: 'Google' },
|
||||
{ id: 'qwen/qwen3-coder-480b-a35b-instruct', name: 'Qwen3 Coder 480B', provider: 'Qwen' },
|
||||
{ id: 'openai/gpt-5.3-codex', name: 'GPT-5.3 Codex', provider: 'OpenAI' },
|
||||
{ id: 'x-ai/grok-code-fast-1', name: 'Grok Code Fast 1', provider: 'xAI' },
|
||||
{ id: 'mistralai/devstral-2-2512', name: 'Devstral 2', provider: 'Mistral' },
|
||||
{ id: 'mistralai/codestral-2508', name: 'Codestral', provider: 'Mistral' },
|
||||
{ id: 'meta-llama/llama-4-maverick', name: 'Llama 4 Maverick', provider: 'Meta' },
|
||||
{ id: 'meta-llama/llama-4-scout', name: 'Llama 4 Scout', provider: 'Meta' },
|
||||
{ id: 'meta-llama/llama-3.3-70b-instruct', name: 'Llama 3.3 70B', provider: 'Meta' },
|
||||
{ id: 'qwen/qwen3-235b-a22b', name: 'Qwen3 235B', provider: 'Qwen' },
|
||||
{ id: 'google/gemma-3-12b-it', name: 'Gemma 3 12B', provider: 'Google' },
|
||||
{ id: 'anthropic/claude-sonnet-4.6', name: 'Claude Sonnet 4.6', provider: 'Anthropic' },
|
||||
{ id: 'openai/gpt-4.1', name: 'GPT-4.1', provider: 'OpenAI' },
|
||||
{ id: 'google/gemini-2.5-flash-preview', name: 'Gemini 2.5 Flash', provider: 'Google' },
|
||||
{ id: 'deepseek/deepseek-chat-v3-0324', name: 'DeepSeek V3', provider: 'DeepSeek' },
|
||||
{ id: 'cohere/command-a', name: 'Command A', provider: 'Cohere' },
|
||||
{ id: 'nousresearch/hermes-3-llama-3.1-405b', name: 'Hermes 3 405B', provider: 'Nous' },
|
||||
{ id: 'perplexity/sonar-deep-research', name: 'Sonar Deep Research', provider: 'Perplexity' },
|
||||
{ id: 'perplexity/sonar-pro', name: 'Sonar Pro', provider: 'Perplexity' },
|
||||
{ id: 'openrouter/free', name: 'Free (auto-pick)', provider: 'OpenRouter' },
|
||||
{ id: 'openrouter/auto', name: 'Auto (best for price)', provider: 'OpenRouter' }
|
||||
{ id: 'meta-llama/llama-3.3-70b-instruct', name: 'Llama 3.3 70B', provider: 'Meta' },
|
||||
{ id: 'nousresearch/hermes-3-llama-3.1-405b', name: 'Hermes 3 405B', provider: 'Nous' }
|
||||
];
|
||||
|
||||
/** @deprecated Use OPENROUTER_MODELS_FALLBACK or OpenRouterModels.fetch() */
|
||||
window.OPENROUTER_MODELS = window.OPENROUTER_MODELS_FALLBACK;
|
||||
|
||||
@@ -13,9 +13,6 @@ class AntiClassifierTool extends Tool {
|
||||
}
|
||||
|
||||
getVueData() {
|
||||
const models = (typeof window !== 'undefined' && window.OPENROUTER_MODELS && window.OPENROUTER_MODELS.length)
|
||||
? window.OPENROUTER_MODELS
|
||||
: [];
|
||||
const savedTemp = parseFloat(localStorage.getItem('ac-temperature'));
|
||||
const acTemperature = Number.isFinite(savedTemp)
|
||||
? Math.min(2, Math.max(0, savedTemp))
|
||||
@@ -26,8 +23,7 @@ class AntiClassifierTool extends Tool {
|
||||
acError: '',
|
||||
acLexemeAnalysis: { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' },
|
||||
acLoading: false,
|
||||
acModel: localStorage.getItem('ac-model') || 'anthropic/claude-sonnet-4.6',
|
||||
acModels: models,
|
||||
acModel: localStorage.getItem('ac-model') || 'openrouter/auto',
|
||||
acTemperature,
|
||||
acMaxTokens: 2000
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ class PromptCraftTool extends Tool {
|
||||
pcOutputs: [],
|
||||
pcLexemeAnalysis: { totalFindings: 0, findings: [], summary: 'No Latin-root wording findings.' },
|
||||
pcStrategy: 'rephrase',
|
||||
pcModel: localStorage.getItem('pc-model') || 'nousresearch/hermes-3-llama-3.1-405b',
|
||||
pcModel: localStorage.getItem('pc-model') || 'openrouter/auto',
|
||||
pcTemperature,
|
||||
pcCount: 3,
|
||||
pcLoading: false,
|
||||
@@ -39,12 +39,7 @@ class PromptCraftTool extends Tool {
|
||||
{ id: 'metaphor', name: 'Metaphor', icon: 'fa-cloud', desc: 'Express through analogy and metaphor' },
|
||||
{ id: 'fragment', name: 'Fragment', icon: 'fa-puzzle-piece', desc: 'Split across disjointed fragments' },
|
||||
{ id: 'custom', name: 'Custom', icon: 'fa-pen-fancy', desc: 'Your own mutation instruction' }
|
||||
],
|
||||
pcModels: (typeof window !== 'undefined' && window.OPENROUTER_MODELS && window.OPENROUTER_MODELS.length)
|
||||
? window.OPENROUTER_MODELS
|
||||
: [
|
||||
{ id: 'openrouter/auto', name: 'Auto (best for price)', provider: 'OpenRouter' }
|
||||
]
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,31 +13,6 @@ class SpellingAlphabetTool extends Tool {
|
||||
}
|
||||
|
||||
getVueData() {
|
||||
var freeModels = [
|
||||
{ id: 'openrouter/free', name: 'Free (auto-pick)', provider: 'OpenRouter' },
|
||||
{ id: 'google/gemma-3-27b-it:free', name: 'Gemma 3 27B (free)', provider: 'Google' },
|
||||
{ id: 'google/gemma-3-12b-it:free', name: 'Gemma 3 12B (free)', provider: 'Google' },
|
||||
{ id: 'google/gemma-3-4b-it:free', name: 'Gemma 3 4B (free)', provider: 'Google' },
|
||||
{ id: 'meta-llama/llama-3.3-70b-instruct:free', name: 'Llama 3.3 70B (free)', provider: 'Meta' }
|
||||
];
|
||||
var allModels = (typeof window !== 'undefined' && window.OPENROUTER_MODELS && window.OPENROUTER_MODELS.length)
|
||||
? window.OPENROUTER_MODELS
|
||||
: [{ id: 'openrouter/auto', name: 'Auto (best for price)', provider: 'OpenRouter' }];
|
||||
var seen = {};
|
||||
var saModels = [];
|
||||
freeModels.forEach(function(model) {
|
||||
if (!seen[model.id]) {
|
||||
seen[model.id] = true;
|
||||
saModels.push(model);
|
||||
}
|
||||
});
|
||||
allModels.forEach(function(model) {
|
||||
if (!seen[model.id]) {
|
||||
seen[model.id] = true;
|
||||
saModels.push(model);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
saView: 'list',
|
||||
saAlphabets: [],
|
||||
@@ -48,8 +23,7 @@ class SpellingAlphabetTool extends Tool {
|
||||
saLoading: false,
|
||||
saError: '',
|
||||
saModel: localStorage.getItem('sa-model') || 'openrouter/free',
|
||||
saLetters: SpellingAlphabetTransform.LETTERS,
|
||||
saModels: saModels
|
||||
saLetters: SpellingAlphabetTransform.LETTERS
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ class SplitterTool extends Tool {
|
||||
super({
|
||||
id: 'splitter',
|
||||
name: 'Splitter',
|
||||
icon: 'fa-grip-lines',
|
||||
icon: 'fa-scissors',
|
||||
title: 'Split text into multiple copyable messages',
|
||||
order: 8
|
||||
});
|
||||
|
||||
@@ -40,16 +40,6 @@ class TranslateTool extends Tool {
|
||||
translateError: '',
|
||||
translateActiveLang: '',
|
||||
translateModel: localStorage.getItem('translate-model') || 'google/gemma-3-27b-it',
|
||||
translateModels: [
|
||||
{ id: 'google/gemma-3-27b-it', name: 'Gemma 3 27B', note: 'Best quality' },
|
||||
{ id: 'google/gemma-3-12b-it', name: 'Gemma 3 12B', note: 'Fast + good' },
|
||||
{ id: 'google/gemma-3-4b-it', name: 'Gemma 3 4B', note: 'Fastest' },
|
||||
{ id: 'google/gemini-2.5-flash-preview', name: 'Gemini 2.5 Flash', note: 'Google flagship' },
|
||||
{ id: 'google/gemini-2.0-flash-001', name: 'Gemini 2.0 Flash', note: 'Stable' },
|
||||
{ id: 'google/translategemma-27b-it', name: 'TranslateGemma 27B', note: 'Purpose-built (if available)' },
|
||||
{ id: 'google/translategemma-12b-it', name: 'TranslateGemma 12B', note: 'Purpose-built (if available)' },
|
||||
{ id: 'google/translategemma-4b-it', name: 'TranslateGemma 4B', note: 'Purpose-built (if available)' }
|
||||
],
|
||||
translateMainLangs: [
|
||||
{ code: 'es', name: 'Spanish', flag: 'ES' },
|
||||
{ code: 'fr', name: 'French', flag: 'FR' },
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Fetch, cache, and normalize OpenRouter model lists for UI dropdowns.
|
||||
*/
|
||||
window.OpenRouterModels = {
|
||||
CACHE_KEY: 'openrouter-models-cache-v1',
|
||||
CACHE_TTL_MS: 60 * 60 * 1000,
|
||||
|
||||
ENABLED_STORAGE_KEY: 'openrouter-models-disabled-v1',
|
||||
|
||||
VIRTUAL: [
|
||||
{
|
||||
id: 'openrouter/free',
|
||||
name: 'Free router',
|
||||
summary: 'Zero cost — random free model matched to your request',
|
||||
provider: 'OpenRouter',
|
||||
virtual: true,
|
||||
routerKind: 'free'
|
||||
},
|
||||
{
|
||||
id: 'openrouter/auto',
|
||||
name: 'Auto router',
|
||||
summary: 'Smart routing — billed at whichever model is picked',
|
||||
provider: 'OpenRouter',
|
||||
virtual: true,
|
||||
routerKind: 'auto'
|
||||
}
|
||||
],
|
||||
|
||||
getApiKey: function() {
|
||||
try {
|
||||
return (
|
||||
localStorage.getItem('openrouter-api-key') ||
|
||||
localStorage.getItem('plinyos-api-key') ||
|
||||
localStorage.getItem('openrouter_api_key') ||
|
||||
''
|
||||
).trim();
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
getStaticFallback: function() {
|
||||
if (window.OPENROUTER_MODELS_FALLBACK && window.OPENROUTER_MODELS_FALLBACK.length) {
|
||||
return window.OPENROUTER_MODELS_FALLBACK.slice();
|
||||
}
|
||||
if (window.OPENROUTER_MODELS && window.OPENROUTER_MODELS.length) {
|
||||
return window.OPENROUTER_MODELS.slice();
|
||||
}
|
||||
return this.VIRTUAL.slice();
|
||||
},
|
||||
|
||||
isFreePricing: function(pricing) {
|
||||
if (!pricing) return false;
|
||||
return String(pricing.prompt) === '0' && String(pricing.completion) === '0';
|
||||
},
|
||||
|
||||
normalize: function(raw) {
|
||||
var id = raw && raw.id ? raw.id : '';
|
||||
var provider = id.indexOf('/') !== -1 ? id.split('/')[0] : '';
|
||||
return {
|
||||
id: id,
|
||||
name: (raw && raw.name) || id,
|
||||
provider: provider,
|
||||
free: this.isFreePricing(raw && raw.pricing),
|
||||
contextLength: raw && raw.context_length ? raw.context_length : null,
|
||||
pricing: raw && raw.pricing ? raw.pricing : null
|
||||
};
|
||||
},
|
||||
|
||||
formatLabel: function(model) {
|
||||
if (!model) return '';
|
||||
if (model.virtual) {
|
||||
return model.name + ' — ' + (model.summary || model.provider);
|
||||
}
|
||||
var label = model.name || model.id;
|
||||
if (model.provider) {
|
||||
label += ' (' + model.provider + ')';
|
||||
}
|
||||
if (model.free) {
|
||||
label += ' · free';
|
||||
}
|
||||
return label;
|
||||
},
|
||||
|
||||
getRouterHint: function(modelId) {
|
||||
var match = this.VIRTUAL.find(function(model) {
|
||||
return model.id === modelId;
|
||||
});
|
||||
return match ? match.summary : '';
|
||||
},
|
||||
|
||||
loadDisabledIds: function() {
|
||||
try {
|
||||
var raw = localStorage.getItem(this.ENABLED_STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
var parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
saveDisabledIds: function(ids) {
|
||||
try {
|
||||
localStorage.setItem(this.ENABLED_STORAGE_KEY, JSON.stringify(ids || []));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save OpenRouter model preferences:', e);
|
||||
}
|
||||
},
|
||||
|
||||
isModelEnabled: function(modelId, disabledIds) {
|
||||
if (!modelId) return true;
|
||||
disabledIds = disabledIds || [];
|
||||
return disabledIds.indexOf(modelId) === -1;
|
||||
},
|
||||
|
||||
filterForDropdown: function(catalog, disabledIds, pinnedIds) {
|
||||
catalog = catalog || [];
|
||||
disabledIds = disabledIds || [];
|
||||
pinnedIds = pinnedIds || [];
|
||||
|
||||
return catalog.filter(function(model) {
|
||||
if (!model || !model.id) return false;
|
||||
if (pinnedIds.indexOf(model.id) !== -1) return true;
|
||||
return disabledIds.indexOf(model.id) === -1;
|
||||
});
|
||||
},
|
||||
|
||||
getPinnedModelIds: function(context) {
|
||||
context = context || {};
|
||||
var ids = [];
|
||||
['pcModel', 'acModel', 'saModel', 'translateModel'].forEach(function(field) {
|
||||
if (context[field]) ids.push(context[field]);
|
||||
});
|
||||
return ids;
|
||||
},
|
||||
|
||||
loadCache: function(apiKey) {
|
||||
try {
|
||||
var raw = localStorage.getItem(this.CACHE_KEY);
|
||||
if (!raw) return null;
|
||||
var parsed = JSON.parse(raw);
|
||||
if (parsed.apiKey !== (apiKey || '')) return null;
|
||||
if (Date.now() - parsed.fetchedAt > this.CACHE_TTL_MS) return null;
|
||||
return Array.isArray(parsed.models) ? parsed.models : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
saveCache: function(apiKey, models) {
|
||||
try {
|
||||
localStorage.setItem(this.CACHE_KEY, JSON.stringify({
|
||||
apiKey: apiKey || '',
|
||||
fetchedAt: Date.now(),
|
||||
models: models
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn('Failed to cache OpenRouter models:', e);
|
||||
}
|
||||
},
|
||||
|
||||
mergeWithVirtual: function(models) {
|
||||
var seen = {};
|
||||
var merged = [];
|
||||
|
||||
this.VIRTUAL.forEach(function(model) {
|
||||
seen[model.id] = true;
|
||||
merged.push(Object.assign({}, model));
|
||||
});
|
||||
|
||||
(models || []).forEach(function(model) {
|
||||
if (!model || !model.id || seen[model.id]) return;
|
||||
seen[model.id] = true;
|
||||
merged.push(model);
|
||||
});
|
||||
|
||||
return merged;
|
||||
},
|
||||
|
||||
fetch: async function(apiKey, options) {
|
||||
options = options || {};
|
||||
var key = (apiKey || '').trim();
|
||||
|
||||
if (!options.force) {
|
||||
var cached = this.loadCache(key);
|
||||
if (cached && cached.length) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
var url = key
|
||||
? 'https://openrouter.ai/api/v1/models/user'
|
||||
: 'https://openrouter.ai/api/v1/models?sort=most-popular';
|
||||
|
||||
var headers = {};
|
||||
if (key) {
|
||||
headers.Authorization = 'Bearer ' + key;
|
||||
}
|
||||
|
||||
var resp = await fetch(url, { headers: headers });
|
||||
if (!resp.ok) {
|
||||
var error = new Error('Failed to load models (HTTP ' + resp.status + ')');
|
||||
error.status = resp.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
var json = await resp.json();
|
||||
var models = (json.data || []).map(this.normalize.bind(this));
|
||||
models = this.mergeWithVirtual(models);
|
||||
this.saveCache(key, models);
|
||||
return models;
|
||||
},
|
||||
|
||||
fetchKeyInfo: async function(apiKey) {
|
||||
var key = (apiKey || '').trim();
|
||||
if (!key) return null;
|
||||
|
||||
try {
|
||||
var resp = await fetch('https://openrouter.ai/api/v1/key', {
|
||||
headers: { Authorization: 'Bearer ' + key }
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
var json = await resp.json();
|
||||
return json.data || null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
ensureValidSelection: function(currentId, models, preferredId) {
|
||||
if (!models || !models.length) return currentId || preferredId || '';
|
||||
if (currentId && models.some(function(model) { return model.id === currentId; })) {
|
||||
return currentId;
|
||||
}
|
||||
if (preferredId && models.some(function(model) { return model.id === preferredId; })) {
|
||||
return preferredId;
|
||||
}
|
||||
return models[0].id;
|
||||
}
|
||||
};
|
||||
+75
-22
@@ -1,37 +1,90 @@
|
||||
/**
|
||||
* Theme registry and application.
|
||||
* Add entries to `themes` to expose new themes in the UI dropdown.
|
||||
*/
|
||||
window.ThemeUtils = {
|
||||
toggleTheme(currentTheme) {
|
||||
const newTheme = !currentTheme;
|
||||
|
||||
if (newTheme) {
|
||||
document.body.classList.add('dark-theme');
|
||||
document.body.classList.remove('light-theme');
|
||||
} else {
|
||||
document.body.classList.add('light-theme');
|
||||
document.body.classList.remove('dark-theme');
|
||||
defaultTheme: 'dark',
|
||||
|
||||
themes: [
|
||||
{ id: 'dark', name: 'Dark', icon: 'fa-moon' },
|
||||
{ id: 'light', name: 'Light', icon: 'fa-sun' },
|
||||
{ id: 'accessible', name: 'Accessible', icon: 'fa-universal-access' },
|
||||
{ id: 'bt6', name: 'BT6', icon: 'fa-shield-halved' },
|
||||
{ id: 'pliny', name: 'Pliny', icon: 'fa-dragon' },
|
||||
{ id: 'cyberpunk', name: 'Cyberpunk', icon: 'fa-city' },
|
||||
{ id: 'wildwest', name: 'Wild West', icon: 'fa-hat-cowboy' }
|
||||
],
|
||||
|
||||
getThemes() {
|
||||
return this.themes.slice();
|
||||
},
|
||||
|
||||
isValidTheme(themeId) {
|
||||
return this.themes.some(function(theme) {
|
||||
return theme.id === themeId;
|
||||
});
|
||||
},
|
||||
|
||||
normalizeThemeId(themeId) {
|
||||
if (this.isValidTheme(themeId)) {
|
||||
return themeId;
|
||||
}
|
||||
|
||||
return this.defaultTheme;
|
||||
},
|
||||
|
||||
applyTheme(themeId) {
|
||||
var resolved = this.normalizeThemeId(themeId);
|
||||
var body = document.body;
|
||||
|
||||
this.themes.forEach(function(theme) {
|
||||
body.classList.remove('theme-' + theme.id);
|
||||
});
|
||||
body.classList.remove('dark-theme', 'light-theme');
|
||||
|
||||
body.classList.add('theme-' + resolved);
|
||||
|
||||
// Legacy aliases used by a few rules / older saved state
|
||||
if (resolved === 'light' || resolved === 'accessible') {
|
||||
body.classList.add('light-theme');
|
||||
} else {
|
||||
body.classList.add('dark-theme');
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem('theme', newTheme ? 'dark' : 'light');
|
||||
localStorage.setItem('theme', resolved);
|
||||
} catch (e) {
|
||||
console.warn('Failed to save theme preference:', e);
|
||||
}
|
||||
|
||||
return newTheme;
|
||||
|
||||
return resolved;
|
||||
},
|
||||
|
||||
|
||||
initializeTheme() {
|
||||
try {
|
||||
const saved = localStorage.getItem('theme');
|
||||
if (saved === 'light') {
|
||||
return false;
|
||||
} else if (saved === 'dark') {
|
||||
return true;
|
||||
var saved = localStorage.getItem('theme');
|
||||
if (saved) {
|
||||
return this.normalizeThemeId(saved);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load theme preference:', e);
|
||||
}
|
||||
|
||||
return true;
|
||||
return this.defaultTheme;
|
||||
},
|
||||
|
||||
cycleTheme(currentThemeId) {
|
||||
var current = this.normalizeThemeId(currentThemeId);
|
||||
var index = this.themes.findIndex(function(theme) {
|
||||
return theme.id === current;
|
||||
});
|
||||
if (index === -1) {
|
||||
index = 0;
|
||||
}
|
||||
var next = this.themes[(index + 1) % this.themes.length];
|
||||
return this.applyTheme(next.id);
|
||||
},
|
||||
|
||||
/** Apply saved theme before Vue mounts (call from inline script in index). */
|
||||
applyInitialTheme() {
|
||||
return this.applyTheme(this.initializeTheme());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "p4rs3lt0ngv3",
|
||||
"version": "1.0.0",
|
||||
"version": "4.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "p4rs3lt0ngv3",
|
||||
"version": "1.0.0",
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@zxing/library": "^0.23.0",
|
||||
@@ -1229,7 +1229,7 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "p4rs3lt0ngv3",
|
||||
"version": "1.0.0",
|
||||
"version": "4.0.0",
|
||||
"description": "Universal Text Encoder/Decoder & Steganography Tool",
|
||||
"scripts": {
|
||||
"build:codes-vendor": "node build/build-code-vendor.js",
|
||||
|
||||
@@ -56,12 +56,7 @@
|
||||
</div>
|
||||
|
||||
<div class="options-grid ac-options">
|
||||
<label>
|
||||
Model
|
||||
<select v-model="acModel">
|
||||
<option v-for="m in acModels" :key="m.id" :value="m.id">{{ m.name }} ({{ m.provider }})</option>
|
||||
</select>
|
||||
</label>
|
||||
<openrouter-model-select v-model="acModel" label="Model"></openrouter-model-select>
|
||||
<label
|
||||
class="slider-block"
|
||||
title="Sampling randomness: lower is more deterministic, higher is more varied (OpenRouter 0–2)."
|
||||
|
||||
@@ -52,8 +52,10 @@
|
||||
</div>
|
||||
|
||||
<div class="options-grid bij-options">
|
||||
<label>
|
||||
Bijection type
|
||||
<label class="bij-field bij-field-type">
|
||||
<span class="bij-field-text">
|
||||
<span class="bij-field-name">Bijection type</span>
|
||||
</span>
|
||||
<select v-model="bijectionType">
|
||||
<option value="char-to-num">Character → Number</option>
|
||||
<option value="char-to-symbol">Character → Symbol</option>
|
||||
@@ -65,19 +67,25 @@
|
||||
<option value="rot-variant">ROT variant</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Fixed size (unchanged leading chars)
|
||||
<label class="bij-field bij-field-narrow">
|
||||
<span class="bij-field-text">
|
||||
<span class="bij-field-name">Fixed size</span>
|
||||
<small class="bij-field-hint">Unchanged leading chars</small>
|
||||
</span>
|
||||
<input type="number" v-model.number="bijectionFixedSize" min="0" max="10" />
|
||||
</label>
|
||||
<label>
|
||||
Attack budget (variants)
|
||||
<label class="bij-field bij-field-narrow">
|
||||
<span class="bij-field-text">
|
||||
<span class="bij-field-name">Attack budget</span>
|
||||
<small class="bij-field-hint">Prompt variants</small>
|
||||
</span>
|
||||
<input type="number" v-model.number="bijectionBudget" min="1" max="50" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="options-grid bij-toggles">
|
||||
<label class="switch neon"><input type="checkbox" v-model="bijectionIncludeExamples" /><span>Include example conversation</span></label>
|
||||
<label class="switch neon"><input type="checkbox" v-model="bijectionAutoCopy" /><span>Auto-copy first prompt</span></label>
|
||||
<label class="switch neon"><input type="checkbox" v-model="bijectionIncludeExamples" /><span>Example conversation</span></label>
|
||||
<label class="switch neon"><input type="checkbox" v-model="bijectionAutoCopy" /><span>Auto-copy prompt</span></label>
|
||||
</div>
|
||||
|
||||
<div class="tool-toolbar">
|
||||
|
||||
@@ -85,12 +85,7 @@
|
||||
</div>
|
||||
|
||||
<div class="options-grid pc-options">
|
||||
<label>
|
||||
Model
|
||||
<select v-model="pcModel">
|
||||
<option v-for="m in pcModels" :key="m.id" :value="m.id">{{ m.name }} ({{ m.provider }})</option>
|
||||
</select>
|
||||
</label>
|
||||
<openrouter-model-select v-model="pcModel" label="Model"></openrouter-model-select>
|
||||
<label>
|
||||
Variants
|
||||
<input type="number" v-model.number="pcCount" min="1" max="10" />
|
||||
|
||||
@@ -63,14 +63,7 @@
|
||||
Category / theme
|
||||
<input type="text" v-model="saCategory" placeholder="e.g. nautical, cooking, astronomy" />
|
||||
</label>
|
||||
<label v-if="saHasApiKey()">
|
||||
OpenRouter model
|
||||
<select v-model="saModel">
|
||||
<option v-for="model in saModels" :key="model.id" :value="model.id">
|
||||
{{ model.name }} ({{ model.provider }})
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<openrouter-model-select v-if="saHasApiKey()" v-model="saModel" label="OpenRouter model"></openrouter-model-select>
|
||||
</div>
|
||||
|
||||
<div class="sa-generate-row">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="transform-section">
|
||||
<div class="section-header-card">
|
||||
<div class="section-header-card-title">
|
||||
<i class="fas fa-grip-lines"></i>
|
||||
<i class="fas fa-scissors"></i>
|
||||
<h3>Message Splitter</h3>
|
||||
</div>
|
||||
<p class="section-header-card-description">Split text into multiple copyable chunks. Each message can be transformed and encapsulated individually.</p>
|
||||
@@ -161,7 +161,7 @@
|
||||
<!-- Generate Button -->
|
||||
<div class="tool-toolbar">
|
||||
<button type="button" class="transform-button tool-primary-btn" @click="generateSplitMessages">
|
||||
<i class="fas fa-cut"></i> Split Messages
|
||||
<i class="fas fa-scissors"></i> Split Messages
|
||||
</button>
|
||||
<button class="action-button copy" v-if="splitMessages.length" @click="copyAllSplitMessages">
|
||||
<i class="fas fa-copy"></i> Copy All
|
||||
|
||||
+90
-70
@@ -19,48 +19,62 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="tokenade-presets">
|
||||
<button class="transform-button" title="Very light density" @click="applyTokenadePreset('feather')">🪶 Featherweight</button>
|
||||
<button class="transform-button" title="Light density" @click="applyTokenadePreset('light')">🍃 Lightweight</button>
|
||||
<button class="transform-button" title="Balanced density" @click="applyTokenadePreset('middle')">🪨 Middleweight</button>
|
||||
<button class="transform-button" title="High density" @click="applyTokenadePreset('heavy')">🗿 Heavyweight</button>
|
||||
<button class="transform-button" title="Extreme density (use with care)" @click="applyTokenadePreset('super')">⚓ Super Heavyweight</button>
|
||||
<button class="transform-button" title="Very light density" @click="applyTokenadePreset('feather')">🪶 Feather</button>
|
||||
<button class="transform-button" title="Light density" @click="applyTokenadePreset('light')">🍃 Light</button>
|
||||
<button class="transform-button" title="Balanced density" @click="applyTokenadePreset('middle')">🪨 Middle</button>
|
||||
<button class="transform-button" title="High density" @click="applyTokenadePreset('heavy')">🗿 Heavy</button>
|
||||
<button class="transform-button" title="Extreme density (use with care)" @click="applyTokenadePreset('super')">⚓ Super</button>
|
||||
</div>
|
||||
<div class="token-bomb-controls options-grid hacker-controls">
|
||||
<label class="slider-block" title="Nesting levels; higher = more layers of grouping. Tip: Increasing depth/breadth grows size multiplicatively.">
|
||||
Depth (nesting)
|
||||
<input type="range" v-model.number="tbDepth" min="1" max="8" />
|
||||
<span class="slider-value">{{ tbDepth }}</span>
|
||||
</label>
|
||||
<label class="slider-block" title="How many items per level; higher = wider structure. Tip: Increasing depth/breadth grows size multiplicatively.">
|
||||
Breadth per level
|
||||
<input type="range" v-model.number="tbBreadth" min="1" max="10" />
|
||||
<span class="slider-value">{{ tbBreadth }}</span>
|
||||
</label>
|
||||
<label class="slider-block" title="How many times to repeat the whole block">
|
||||
Repeats (blocks)
|
||||
<input type="range" v-model.number="tbRepeats" min="1" max="50" />
|
||||
<span class="slider-value">{{ tbRepeats }}</span>
|
||||
</label>
|
||||
<label class="switch neon" title="Adds VS16/VS15 after glyphs to increase token churn">
|
||||
<input type="checkbox" v-model="tbIncludeVS" />
|
||||
<span>Variation Selectors</span>
|
||||
</label>
|
||||
<label class="switch neon" title="Sprinkles random zero-width codepoints between segments">
|
||||
<input type="checkbox" v-model="tbIncludeNoise" />
|
||||
<span>Invisible Noise</span>
|
||||
</label>
|
||||
<!-- Randomize always on by default; control removed from UI -->
|
||||
<label class="switch neon" title="Automatically copy result after generation">
|
||||
<input type="checkbox" v-model="tbAutoCopy" />
|
||||
<span>Auto-copy</span>
|
||||
</label>
|
||||
<div class="segmented" title="Invisible separator inserted between units">
|
||||
<div class="segmented-label">Separator</div>
|
||||
<div>
|
||||
<button :class="{active: tbSeparator==='zwj'}" @click="tbSeparator='zwj'">ZWJ</button>
|
||||
<button :class="{active: tbSeparator==='zwnj'}" @click="tbSeparator='zwnj'">ZWNJ</button>
|
||||
<button :class="{active: tbSeparator==='zwsp'}" @click="tbSeparator='zwsp'">ZWSP</button>
|
||||
<button :class="{active: tbSeparator==='none'}" @click="tbSeparator='none'">None</button>
|
||||
<div class="tokenade-controls">
|
||||
<div class="options-grid tokenade-sliders">
|
||||
<label class="slider-block" title="Nesting levels; higher = more layers of grouping. Tip: Increasing depth/breadth grows size multiplicatively.">
|
||||
<span class="tokenade-field-text">
|
||||
<span class="tokenade-field-name">Depth</span>
|
||||
<small class="tokenade-field-hint">Nesting levels</small>
|
||||
</span>
|
||||
<input type="range" v-model.number="tbDepth" min="1" max="8" />
|
||||
<span class="slider-value">{{ tbDepth }}</span>
|
||||
</label>
|
||||
<label class="slider-block" title="How many items per level; higher = wider structure. Tip: Increasing depth/breadth grows size multiplicatively.">
|
||||
<span class="tokenade-field-text">
|
||||
<span class="tokenade-field-name">Breadth</span>
|
||||
<small class="tokenade-field-hint">Items per level</small>
|
||||
</span>
|
||||
<input type="range" v-model.number="tbBreadth" min="1" max="10" />
|
||||
<span class="slider-value">{{ tbBreadth }}</span>
|
||||
</label>
|
||||
<label class="slider-block" title="How many times to repeat the whole block">
|
||||
<span class="tokenade-field-text">
|
||||
<span class="tokenade-field-name">Repeats</span>
|
||||
<small class="tokenade-field-hint">Block count</small>
|
||||
</span>
|
||||
<input type="range" v-model.number="tbRepeats" min="1" max="50" />
|
||||
<span class="slider-value">{{ tbRepeats }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-grid tokenade-toggles">
|
||||
<label class="switch neon" title="Adds VS16/VS15 after glyphs to increase token churn">
|
||||
<input type="checkbox" v-model="tbIncludeVS" />
|
||||
<span>Variation selectors</span>
|
||||
</label>
|
||||
<label class="switch neon" title="Sprinkles random zero-width codepoints between segments">
|
||||
<input type="checkbox" v-model="tbIncludeNoise" />
|
||||
<span>Invisible noise</span>
|
||||
</label>
|
||||
<label class="switch neon" title="Automatically copy result after generation">
|
||||
<input type="checkbox" v-model="tbAutoCopy" />
|
||||
<span>Auto-copy</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="tokenade-separator-row">
|
||||
<div class="segmented" title="Invisible separator inserted between units">
|
||||
<div class="segmented-label">Separator</div>
|
||||
<div class="segmented-buttons">
|
||||
<button type="button" :class="{active: tbSeparator==='zwj'}" @click="tbSeparator='zwj'">ZWJ</button>
|
||||
<button type="button" :class="{active: tbSeparator==='zwnj'}" @click="tbSeparator='zwnj'">ZWNJ</button>
|
||||
<button type="button" :class="{active: tbSeparator==='zwsp'}" @click="tbSeparator='zwsp'">ZWSP</button>
|
||||
<button type="button" :class="{active: tbSeparator==='none'}" @click="tbSeparator='none'">None</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,33 +83,37 @@
|
||||
Estimated length: {{ estimateTokenadeLength().toLocaleString() }} chars
|
||||
</small>
|
||||
</div>
|
||||
<div class="tokenade-carrier-options options-grid">
|
||||
<div class="carrier-quick-grid" v-if="tbSingleCarrier">
|
||||
<small>Quick picks</small>
|
||||
<div class="emoji-grid">
|
||||
<button class="emoji-button" v-for="em in quickCarrierEmojis" :key="'q-'+em" @click="tbCarrier = em" :title="'Use '+em">{{ em }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="switch neon" title="Condense payload into a single emoji carrier">
|
||||
<div class="tokenade-carrier-options">
|
||||
<label class="switch neon tokenade-carrier-toggle" title="Condense payload into a single emoji carrier">
|
||||
<input type="checkbox" v-model="tbSingleCarrier" />
|
||||
<span>Single emoji carrier mode</span>
|
||||
</label>
|
||||
<label v-if="tbSingleCarrier">
|
||||
<span class="label-with-tooltip">
|
||||
Carrier
|
||||
<i class="fas fa-info-circle tooltip-icon" data-tooltip="Choose the carrier emoji (quick list)"></i>
|
||||
</span>
|
||||
<select v-model="tbCarrier" @focus="setCarrierFromSelected">
|
||||
<option v-for="(em, index) in carrierEmojiList" :key="'carrier-'+index" :value="em">{{ em }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-if="tbSingleCarrier">
|
||||
<span class="label-with-tooltip">
|
||||
Advanced: custom carrier text
|
||||
<i class="fas fa-info-circle tooltip-icon" data-tooltip="When set, this overrides quick picks and dropdown."></i>
|
||||
</span>
|
||||
<input type="text" v-model="tbCarrierManual" placeholder="e.g., ⚙️ or ::tag::" />
|
||||
<span>Single emoji carrier</span>
|
||||
</label>
|
||||
<template v-if="tbSingleCarrier">
|
||||
<div class="carrier-quick-grid">
|
||||
<small>Quick picks</small>
|
||||
<div class="emoji-grid">
|
||||
<button class="emoji-button" v-for="em in quickCarrierEmojis" :key="'q-'+em" @click="tbCarrier = em" :title="'Use '+em">{{ em }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-grid tokenade-carrier-fields">
|
||||
<label>
|
||||
<span class="label-with-tooltip">
|
||||
Carrier
|
||||
<i class="fas fa-info-circle tooltip-icon" data-tooltip="Choose the carrier emoji (quick list)"></i>
|
||||
</span>
|
||||
<select v-model="tbCarrier" @focus="setCarrierFromSelected">
|
||||
<option v-for="(em, index) in carrierEmojiList" :key="'carrier-'+index" :value="em">{{ em }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="tokenade-field-text">
|
||||
<span class="tokenade-field-name">Custom carrier</span>
|
||||
<small class="tokenade-field-hint">Overrides quick picks</small>
|
||||
</span>
|
||||
<input type="text" v-model="tbCarrierManual" placeholder="e.g., ⚙️ or ::tag::" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="tool-toolbar">
|
||||
<button type="button" class="transform-button tool-primary-btn" @click="generateTokenBomb" title="Build the tokenade with current settings">
|
||||
@@ -122,18 +140,20 @@
|
||||
Base text
|
||||
<input type="text" v-model="tpBase" placeholder="Enter base snippet (e.g., hello)" />
|
||||
</label>
|
||||
<div class="options-grid">
|
||||
<div class="options-grid tokenade-text-options">
|
||||
<label>
|
||||
Repeat count
|
||||
<span class="tokenade-field-text">
|
||||
<span class="tokenade-field-name">Repeat count</span>
|
||||
</span>
|
||||
<input type="number" v-model.number="tpRepeat" min="1" max="1000" />
|
||||
</label>
|
||||
<label class="switch neon">
|
||||
<input type="checkbox" v-model="tpCombining" />
|
||||
<span>Add combining marks</span>
|
||||
<span>Combining marks</span>
|
||||
</label>
|
||||
<label class="switch neon">
|
||||
<input type="checkbox" v-model="tpZW" />
|
||||
<span>Add zero-width spacing</span>
|
||||
<span>Zero-width spacing</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="tool-toolbar">
|
||||
|
||||
+176
-53
@@ -89,7 +89,7 @@
|
||||
:class="{ active: activeTransform === item.transform }"
|
||||
:title="'Click to transform and copy: ' + item.transform.name"
|
||||
>
|
||||
{{ item.transform.name }}
|
||||
<span class="transform-button-name">{{ item.transform.name }}</span>
|
||||
<small class="transform-preview" v-if="transformInput">
|
||||
{{ item.transform.preview(transformInput.slice(0, 10), getMergedOptionsForTransform(item.transform.name)) }}
|
||||
</small>
|
||||
@@ -106,6 +106,23 @@
|
||||
:title="isFavorite(item.transform.name) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<div class="transform-card-actions">
|
||||
<button
|
||||
v-if="transformHasOptionsUI(item.transform)"
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-options"
|
||||
@click.stop="openTransformOptions(item.transform, $event)"
|
||||
:aria-label="'Options for ' + item.transform.name"
|
||||
>Options</button>
|
||||
<span v-else class="transform-card-action-slot" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-favorite"
|
||||
:class="{ 'is-favorited': isFavorite(item.transform.name) }"
|
||||
@click.stop="toggleFavorite(item.transform.name, $event)"
|
||||
:aria-pressed="isFavorite(item.transform.name) ? 'true' : 'false'"
|
||||
>{{ isFavorite(item.transform.name) ? 'Favorited' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'translate'" class="transform-button-group">
|
||||
<button
|
||||
@@ -115,7 +132,7 @@
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + item.langName"
|
||||
>
|
||||
{{ item.langName }}
|
||||
<span class="transform-button-name">{{ item.langName }}</span>
|
||||
<small class="transform-preview" v-if="transformInput">
|
||||
{{ transformInput.slice(0, 10) }}
|
||||
</small>
|
||||
@@ -126,6 +143,16 @@
|
||||
:title="isTranslateFavorite(item.langName, item.custom) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<div class="transform-card-actions">
|
||||
<span class="transform-card-action-slot" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-favorite"
|
||||
:class="{ 'is-favorited': isTranslateFavorite(item.langName, item.custom) }"
|
||||
@click.stop="toggleTranslateFavorite(item.langName, item.custom, $event)"
|
||||
:aria-pressed="isTranslateFavorite(item.langName, item.custom) ? 'true' : 'false'"
|
||||
>{{ isTranslateFavorite(item.langName, item.custom) ? 'Favorited' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -149,7 +176,7 @@
|
||||
:class="{ active: activeTransform === item.transform }"
|
||||
:title="'Click to transform and copy: ' + item.transform.name"
|
||||
>
|
||||
{{ item.transform.name }}
|
||||
<span class="transform-button-name">{{ item.transform.name }}</span>
|
||||
<small class="transform-preview" v-if="transformInput">
|
||||
{{ item.transform.preview(transformInput.slice(0, 10), getMergedOptionsForTransform(item.transform.name)) }}
|
||||
</small>
|
||||
@@ -166,6 +193,23 @@
|
||||
:title="isFavorite(item.transform.name) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<div class="transform-card-actions">
|
||||
<button
|
||||
v-if="transformHasOptionsUI(item.transform)"
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-options"
|
||||
@click.stop="openTransformOptions(item.transform, $event)"
|
||||
:aria-label="'Options for ' + item.transform.name"
|
||||
>Options</button>
|
||||
<span v-else class="transform-card-action-slot" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-favorite"
|
||||
:class="{ 'is-favorited': isFavorite(item.transform.name) }"
|
||||
@click.stop="toggleFavorite(item.transform.name, $event)"
|
||||
:aria-pressed="isFavorite(item.transform.name) ? 'true' : 'false'"
|
||||
>{{ isFavorite(item.transform.name) ? 'Favorited' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="item.type === 'translate'" class="transform-button-group">
|
||||
<button
|
||||
@@ -175,7 +219,7 @@
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + item.langName"
|
||||
>
|
||||
{{ item.langName }}
|
||||
<span class="transform-button-name">{{ item.langName }}</span>
|
||||
<small class="transform-preview" v-if="transformInput">
|
||||
{{ transformInput.slice(0, 10) }}
|
||||
</small>
|
||||
@@ -186,6 +230,16 @@
|
||||
:title="isTranslateFavorite(item.langName, item.custom) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<div class="transform-card-actions">
|
||||
<span class="transform-card-action-slot" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-favorite"
|
||||
:class="{ 'is-favorited': isTranslateFavorite(item.langName, item.custom) }"
|
||||
@click.stop="toggleTranslateFavorite(item.langName, item.custom, $event)"
|
||||
:aria-pressed="isTranslateFavorite(item.langName, item.custom) ? 'true' : 'false'"
|
||||
>{{ isTranslateFavorite(item.langName, item.custom) ? 'Favorited' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -206,55 +260,79 @@
|
||||
<i class="fas fa-spinner fa-spin"></i> Translating to {{ translateActiveLang }}...
|
||||
</div>
|
||||
|
||||
<div class="translate-model-picker">
|
||||
<select v-model="translateModel" class="translate-model-select">
|
||||
<option v-for="m in translateModels" :key="m.id" :value="m.id">{{ m.name }} — {{ m.note }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<openrouter-model-select v-model="translateModel" label="Translation model"></openrouter-model-select>
|
||||
|
||||
<div class="translate-subsection">
|
||||
<div class="translate-subsection-label"><i class="fas fa-globe"></i> Major</div>
|
||||
<div class="translate-lang-grid translate-lang-grid-inline">
|
||||
<button
|
||||
<div
|
||||
v-for="lang in translateMainLangs"
|
||||
:key="lang.code"
|
||||
class="translate-lang-btn translate-lang-main"
|
||||
@click="translateTo(lang.name)"
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + lang.name"
|
||||
class="translate-lang-card"
|
||||
>
|
||||
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
|
||||
<span class="translate-name">{{ lang.name }}</span>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
|
||||
class="fas fa-star favorite-icon translate-lang-favorite"
|
||||
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
|
||||
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
class="translate-lang-btn translate-lang-main"
|
||||
@click="translateTo(lang.name)"
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + lang.name"
|
||||
>
|
||||
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
|
||||
<span class="translate-name">{{ lang.name }}</span>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
|
||||
class="fas fa-star favorite-icon translate-lang-favorite"
|
||||
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
|
||||
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<div class="transform-card-actions">
|
||||
<span class="transform-card-action-slot" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-favorite"
|
||||
:class="{ 'is-favorited': isTranslateFavorite(lang.name, false) }"
|
||||
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
|
||||
:aria-pressed="isTranslateFavorite(lang.name, false) ? 'true' : 'false'"
|
||||
>{{ isTranslateFavorite(lang.name, false) ? 'Favorited' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="translate-subsection">
|
||||
<div class="translate-subsection-label"><i class="fas fa-scroll"></i> Dead & Exotic</div>
|
||||
<div class="translate-lang-grid translate-lang-grid-inline">
|
||||
<button
|
||||
<div
|
||||
v-for="lang in translateExoticLangs"
|
||||
:key="lang.code"
|
||||
class="translate-lang-btn translate-lang-exotic"
|
||||
@click="translateTo(lang.name)"
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + lang.name"
|
||||
class="translate-lang-card"
|
||||
>
|
||||
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
|
||||
<span class="translate-name">{{ lang.name }}</span>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
|
||||
class="fas fa-star favorite-icon translate-lang-favorite"
|
||||
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
|
||||
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
class="translate-lang-btn translate-lang-exotic"
|
||||
@click="translateTo(lang.name)"
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + lang.name"
|
||||
>
|
||||
<span class="translate-flag">{{ translateGetFlag(lang.flag) }}</span>
|
||||
<span class="translate-name">{{ lang.name }}</span>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
|
||||
class="fas fa-star favorite-icon translate-lang-favorite"
|
||||
:class="{ favorited: isTranslateFavorite(lang.name, false) }"
|
||||
:title="isTranslateFavorite(lang.name, false) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<div class="transform-card-actions">
|
||||
<span class="transform-card-action-slot" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-favorite"
|
||||
:class="{ 'is-favorited': isTranslateFavorite(lang.name, false) }"
|
||||
@click.stop="toggleTranslateFavorite(lang.name, false, $event)"
|
||||
:aria-pressed="isTranslateFavorite(lang.name, false) ? 'true' : 'false'"
|
||||
>{{ isTranslateFavorite(lang.name, false) ? 'Favorited' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -283,24 +361,42 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="translate-lang-grid translate-lang-grid-inline" v-if="translateCustomLangs.length">
|
||||
<button
|
||||
<div
|
||||
v-for="(lang, idx) in translateCustomLangs"
|
||||
:key="'custom-'+idx"
|
||||
class="translate-lang-btn translate-lang-custom"
|
||||
@click="translateTo(lang.name)"
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + lang.name"
|
||||
class="translate-lang-card"
|
||||
>
|
||||
<span class="translate-flag translate-custom-flag" aria-hidden="true"><i class="fas fa-globe"></i></span>
|
||||
<span class="translate-name">{{ lang.name }}</span>
|
||||
<span class="translate-remove" @click.stop="translateRemoveCustomLang(idx)" title="Remove">×</span>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(lang.name, true, $event)"
|
||||
class="fas fa-star favorite-icon translate-lang-favorite"
|
||||
:class="{ favorited: isTranslateFavorite(lang.name, true) }"
|
||||
:title="isTranslateFavorite(lang.name, true) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<button
|
||||
class="translate-lang-btn translate-lang-custom"
|
||||
@click="translateTo(lang.name)"
|
||||
:disabled="translateLoading"
|
||||
:title="'Translate to ' + lang.name"
|
||||
>
|
||||
<span class="translate-flag translate-custom-flag" aria-hidden="true"><i class="fas fa-globe"></i></span>
|
||||
<span class="translate-name">{{ lang.name }}</span>
|
||||
<span class="translate-remove" @click.stop="translateRemoveCustomLang(idx)" title="Remove">×</span>
|
||||
<i
|
||||
@click.stop="toggleTranslateFavorite(lang.name, true, $event)"
|
||||
class="fas fa-star favorite-icon translate-lang-favorite"
|
||||
:class="{ favorited: isTranslateFavorite(lang.name, true) }"
|
||||
:title="isTranslateFavorite(lang.name, true) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<div class="transform-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action translate-lang-action-remove"
|
||||
@click.stop="translateRemoveCustomLang(idx)"
|
||||
>Remove</button>
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-favorite"
|
||||
:class="{ 'is-favorited': isTranslateFavorite(lang.name, true) }"
|
||||
@click.stop="toggleTranslateFavorite(lang.name, true, $event)"
|
||||
:aria-pressed="isTranslateFavorite(lang.name, true) ? 'true' : 'false'"
|
||||
>{{ isTranslateFavorite(lang.name, true) ? 'Favorited' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!translateCustomLangs.length && !translateAddingLang" class="translate-empty-custom">
|
||||
<small>Click <strong>Custom</strong> above to add any language.</small>
|
||||
@@ -338,6 +434,16 @@
|
||||
:title="isFavorite(transform.name) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<div class="transform-card-actions">
|
||||
<span class="transform-card-action-slot" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-favorite"
|
||||
:class="{ 'is-favorited': isFavorite(transform.name) }"
|
||||
@click.stop="toggleFavorite(transform.name, $event)"
|
||||
:aria-pressed="isFavorite(transform.name) ? 'true' : 'false'"
|
||||
>{{ isFavorite(transform.name) ? 'Favorited' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="randomizer-info">
|
||||
@@ -387,7 +493,7 @@
|
||||
:class="{ active: activeTransform === transform }"
|
||||
:title="'Click to transform and copy: ' + transform.name"
|
||||
>
|
||||
{{ transform.name }}
|
||||
<span class="transform-button-name">{{ transform.name }}</span>
|
||||
<small class="transform-preview" v-if="transformInput">
|
||||
{{ transform.preview(transformInput.slice(0, 10), getMergedOptionsForTransform(transform.name)) }}
|
||||
</small>
|
||||
@@ -404,6 +510,23 @@
|
||||
:title="isFavorite(transform.name) ? 'Remove from favorites' : 'Add to favorites'"
|
||||
></i>
|
||||
</button>
|
||||
<div class="transform-card-actions">
|
||||
<button
|
||||
v-if="transformHasOptionsUI(transform)"
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-options"
|
||||
@click.stop="openTransformOptions(transform, $event)"
|
||||
:aria-label="'Options for ' + transform.name"
|
||||
>Options</button>
|
||||
<span v-else class="transform-card-action-slot" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class="transform-card-action transform-card-action-favorite"
|
||||
:class="{ 'is-favorited': isFavorite(transform.name) }"
|
||||
@click.stop="toggleFavorite(transform.name, $event)"
|
||||
:aria-pressed="isFavorite(transform.name) ? 'true' : 'false'"
|
||||
>{{ isFavorite(transform.name) ? 'Favorited' : 'Favorite' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user