Compare commits
19
Commits
c9d604ab68
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03400ad5d5 | ||
|
|
34553c18f7 | ||
|
|
6f906d0d97 | ||
|
|
3b78f284a1 | ||
|
|
6e816e1436 | ||
|
|
28ef82aae7 | ||
|
|
59b125fd2a | ||
|
|
4bd29a207a | ||
|
|
e4bb48d9ea | ||
|
|
8b5cf75396 | ||
|
|
c938916fd8 | ||
|
|
f00475cb65 | ||
|
|
28802b1ad7 | ||
|
|
5b9c1ca0f1 | ||
|
|
df7d3b3a38 | ||
|
|
29f7865927 | ||
|
|
9c0f09619d | ||
|
|
3c4e632906 | ||
|
|
4e4b2328ba |
@@ -2,18 +2,19 @@
|
|||||||
|
|
||||||
## What this is
|
## What this is
|
||||||
|
|
||||||
A **browser extension for SillyTavern** (AI roleplay frontend). It is NOT a standalone Node.js application. All JavaScript runs in the browser context inside SillyTavern. There is **no build step** — files are loaded directly by SillyTavern's extension loader.
|
A **browser extension for SillyTavern** (AI roleplay frontend). It is NOT a standalone Node.js application. All JavaScript runs in the browser context inside SillyTavern. There is **no bundler** — JS files are loaded directly by SillyTavern's extension loader. CSS modules are concatenated via a build script.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
- **Entry point**: `index.js` (loaded by SillyTavern via `manifest.json`)
|
- **Entry point**: `index.js` (loaded by SillyTavern via `manifest.json`)
|
||||||
- **CSS**: `style.css` (single file, ~12,300 lines)
|
- **CSS**: Modular — 28 files in `src/styles/`, concatenated via `npm run build:css` (or `npm run build:css:min` for production)
|
||||||
- **HTML templates**: `template.html` (main panel), `settings.html` (Extensions tab settings drawer)
|
- **HTML templates**: `template.html` (main panel), `settings.html` (Extensions tab settings drawer)
|
||||||
- **Runtime**: Browser only. All code executes inside SillyTavern's page context.
|
- **Runtime**: Browser only. All code executes inside SillyTavern's page context.
|
||||||
- **jQuery**: Available globally as `$` / `jQuery`. Used extensively for DOM manipulation.
|
- **jQuery**: Available globally as `$` / `jQuery`. Used extensively for DOM manipulation.
|
||||||
- **ES modules**: All `src/` files use `import`/`export` (browser ES module syntax).
|
- **ES modules**: All `src/` files use `import`/`export` (browser ES module syntax).
|
||||||
|
- **Testing**: Jest + Babel ESM support — 78 tests (`npm test`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -76,11 +77,12 @@ The deeper relative imports in `src/` files (e.g., `../../../../../../script.js`
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── core/ # Core infrastructure
|
├── core/ # Core infrastructure
|
||||||
│ ├── config.js # Extension name, folder path, default settings
|
│ ├── config.js # Extension name, folder path, default settings (single source of truth)
|
||||||
│ ├── state.js # Centralized state variables + getters/setters
|
│ ├── state.js # Centralized state variables + getters/setters (imports defaults from config.js)
|
||||||
│ ├── persistence.js # Save/load settings, chat data, migrations
|
│ ├── persistence.js # Save/load settings, chat data, migrations
|
||||||
│ ├── events.js # SillyTavern event system wrapper
|
│ ├── events.js # SillyTavern event system wrapper
|
||||||
│ └── i18n.js # Translation system (fetches JSON, applies data-i18n-key)
|
│ ├── i18n.js # Translation system (fetches JSON, applies data-i18n-key)
|
||||||
|
│ └── settingsPanel.js # Extracted settings panel UI (from index.js refactor)
|
||||||
├── i18n/ # Translation JSON files
|
├── i18n/ # Translation JSON files
|
||||||
│ ├── en.json # English (reference, 513 lines)
|
│ ├── en.json # English (reference, 513 lines)
|
||||||
│ ├── fr.json # French
|
│ ├── fr.json # French
|
||||||
@@ -88,10 +90,39 @@ src/
|
|||||||
│ ├── zh-cn.json # Simplified Chinese
|
│ ├── zh-cn.json # Simplified Chinese
|
||||||
│ ├── zh-tw.json # Traditional Chinese
|
│ ├── zh-tw.json # Traditional Chinese
|
||||||
│ └── validator.js # Node.js script to validate translation consistency
|
│ └── validator.js # Node.js script to validate translation consistency
|
||||||
|
├── styles/ # CSS modules (28 files, concatenated via build script)
|
||||||
|
│ ├── _01_variables.css # CSS custom properties
|
||||||
|
│ ├── _02_panel.css # Main panel layout
|
||||||
|
│ ├── _03_components.css # Buttons, inputs, common components
|
||||||
|
│ ├── _04_user-stats.css # User stats panel
|
||||||
|
│ ├── _05_info-box.css # Info box dashboard
|
||||||
|
│ ├── _06_thoughts.css # Character thoughts
|
||||||
|
│ ├── _07_settings.css # Settings modal
|
||||||
|
│ ├── _08_themes.css # Theme definitions (extended custom properties)
|
||||||
|
│ ├── _09_dice-modal.css # Dice roller modal
|
||||||
|
│ ├── _10_tracker-editor.css # Tracker configuration editor
|
||||||
|
│ ├── _11_settings-modal.css # Settings modal styles
|
||||||
|
│ ├── _12_thought-bubbles.css # Floating thought bubbles
|
||||||
|
│ ├── _13_mobile.css # Mobile responsive styles
|
||||||
|
│ ├── _14_inventory.css # Inventory views
|
||||||
|
│ ├── _15_inventory-views.css # Inventory detail views
|
||||||
|
│ ├── _16_desktop-tabs.css # Desktop tab navigation
|
||||||
|
│ ├── _17_quests.css # Quest tracking
|
||||||
|
│ ├── _18_equipment.css # Equipment grid
|
||||||
|
│ ├── _19_encounters.css # Combat encounters
|
||||||
|
│ ├── _20_plot-buttons.css # Plot progression buttons
|
||||||
|
│ ├── _21_music-player.css # Spotify music player
|
||||||
|
│ ├── _22_weather.css # Weather effects
|
||||||
|
│ ├── _23_holiday-promo.css # Holiday promotions
|
||||||
|
│ ├── _24_feature-toggles.css # Feature toggle switches
|
||||||
|
│ ├── _25_import-dialog.css # Import/export dialog
|
||||||
|
│ ├── _26_fab-widgets.css # Mobile FAB widgets
|
||||||
|
│ ├── _27_strip-widgets.css # Collapsed panel strip widgets
|
||||||
|
│ └── _28_equipment-modal.css # Equipment edit modal
|
||||||
├── systems/ # Feature systems (organized by concern)
|
├── systems/ # Feature systems (organized by concern)
|
||||||
│ ├── generation/ # AI prompt generation, parsing, API calls
|
│ ├── generation/ # AI prompt generation, parsing, API calls
|
||||||
│ │ ├── promptBuilder.js # Builds tracker prompts for AI
|
│ │ ├── promptBuilder.js # Builds tracker prompts for AI (refactored with focused helpers)
|
||||||
│ │ ├── parser.js # Parses AI responses to extract tracker data
|
│ │ ├── parser.js # Parses AI responses (~50 pre-compiled regex patterns)
|
||||||
│ │ ├── apiClient.js # Makes API calls for separate/external generation
|
│ │ ├── apiClient.js # Makes API calls for separate/external generation
|
||||||
│ │ ├── injector.js # Injects tracker prompts into generation context
|
│ │ ├── injector.js # Injects tracker prompts into generation context
|
||||||
│ │ ├── lockManager.js # Manages locked tracker fields
|
│ │ ├── lockManager.js # Manages locked tracker fields
|
||||||
@@ -106,7 +137,8 @@ src/
|
|||||||
│ │ ├── inventory.js # Renders inventory section
|
│ │ ├── inventory.js # Renders inventory section
|
||||||
│ │ ├── equipment.js # Renders equipment section
|
│ │ ├── equipment.js # Renders equipment section
|
||||||
│ │ ├── quests.js # Renders quests section
|
│ │ ├── quests.js # Renders quests section
|
||||||
│ │ └── musicPlayer.js # Renders Spotify music player
|
│ │ ├── musicPlayer.js # Renders Spotify music player
|
||||||
|
│ │ └── renderUtils.js # Rendering utilities (updateIfChanged cache, rAF batching)
|
||||||
│ ├── ui/ # UI components and helpers
|
│ ├── ui/ # UI components and helpers
|
||||||
│ │ ├── theme.js # Theme system (default, sci-fi, fantasy, cyberpunk, custom)
|
│ │ ├── theme.js # Theme system (default, sci-fi, fantasy, cyberpunk, custom)
|
||||||
│ │ ├── modals.js # Settings modal, dice modal, deprecation modal
|
│ │ ├── modals.js # Settings modal, dice modal, deprecation modal
|
||||||
@@ -119,7 +151,10 @@ src/
|
|||||||
│ │ ├── encounterUI.js # Combat encounter modal
|
│ │ ├── encounterUI.js # Combat encounter modal
|
||||||
│ │ ├── snowflakes.js # Snowflake animation effect
|
│ │ ├── snowflakes.js # Snowflake animation effect
|
||||||
│ │ ├── weatherEffects.js # Dynamic weather visual effects
|
│ │ ├── weatherEffects.js # Dynamic weather visual effects
|
||||||
│ │ └── alternatePresentCharacters.js # Below-chat character panel
|
│ │ ├── alternatePresentCharacters.js # Below-chat character panel
|
||||||
|
│ │ └── settingsListeners.js # Settings event listeners (extracted from index.js)
|
||||||
|
│ ├── equipment/ # Equipment system
|
||||||
|
│ │ └── constants.js # Equipment slot definitions, type limits, stat config
|
||||||
│ ├── integration/ # SillyTavern event integration
|
│ ├── integration/ # SillyTavern event integration
|
||||||
│ │ ├── sillytavern.js # Event handlers: message sent/received, swipe, etc.
|
│ │ ├── sillytavern.js # Event handlers: message sent/received, swipe, etc.
|
||||||
│ │ └── thoughtBasedExpressions.js # Integrates with ST Character Expressions
|
│ │ └── thoughtBasedExpressions.js # Integrates with ST Character Expressions
|
||||||
@@ -144,11 +179,11 @@ src/
|
|||||||
├── imageUrls.js # Image URL utilities
|
├── imageUrls.js # Image URL utilities
|
||||||
├── itemParser.js # Item string parsing
|
├── itemParser.js # Item string parsing
|
||||||
├── jsonMigration.js # v2 to v3 JSON format migration
|
├── jsonMigration.js # v2 to v3 JSON format migration
|
||||||
├── jsonRepair.js # JSON repair for malformed AI responses
|
├── jsonRepair.js # JSON repair for malformed AI responses (14 pre-compiled regex)
|
||||||
├── migration.js # Settings migration helpers
|
├── migration.js # Settings migration helpers
|
||||||
├── presentCharacters.js # Present character data helpers
|
├── presentCharacters.js # Present character data helpers
|
||||||
├── responseExtractor.js # Extract tracker data from AI responses
|
├── responseExtractor.js # Extract tracker data from AI responses
|
||||||
├── security.js # Input validation and sanitization
|
├── security.js # Input validation and sanitization (validator factory)
|
||||||
├── sillyTavernExpressions.js # ST expression mapping
|
├── sillyTavernExpressions.js # ST expression mapping
|
||||||
├── thoughtBasedExpressionPortraits.js # Expression-to-portrait mapping
|
├── thoughtBasedExpressionPortraits.js # Expression-to-portrait mapping
|
||||||
└── transformations.js # Data transformation utilities
|
└── transformations.js # Data transformation utilities
|
||||||
@@ -163,7 +198,7 @@ src/
|
|||||||
5. **External API mode**: Uses user-configured external API endpoint
|
5. **External API mode**: Uses user-configured external API endpoint
|
||||||
6. **AI responds** → `MESSAGE_RECEIVED` event fires
|
6. **AI responds** → `MESSAGE_RECEIVED` event fires
|
||||||
7. **Extension hooks** `onMessageReceived` → `parser.js` extracts tracker data
|
7. **Extension hooks** `onMessageReceived` → `parser.js` extracts tracker data
|
||||||
8. **Rendering**: `renderUserStats()`, `renderInfoBox()`, `renderThoughts()`, etc. update the UI
|
8. **Rendering**: `renderUserStats()`, `renderInfoBox()`, `renderThoughts()`, etc. update the UI (batched via rAF)
|
||||||
9. **Persistence**: Tracker data is saved to `chat_metadata` per chat, and per-swipe
|
9. **Persistence**: Tracker data is saved to `chat_metadata` per chat, and per-swipe
|
||||||
|
|
||||||
### State management
|
### State management
|
||||||
@@ -178,6 +213,8 @@ All extension state lives in `src/core/state.js` as module-level `let` variables
|
|||||||
- `pendingDiceRoll` — dice roll result to inject into next generation
|
- `pendingDiceRoll` — dice roll result to inject into next generation
|
||||||
- DOM element caches: `$panelContainer`, `$userStatsContainer`, etc.
|
- DOM element caches: `$panelContainer`, `$userStatsContainer`, etc.
|
||||||
|
|
||||||
|
**Settings consolidation**: `config.js` is the single source of truth for default settings. `state.js` imports defaults from `config.js` (~300 lines eliminated from duplication).
|
||||||
|
|
||||||
### Persistence
|
### Persistence
|
||||||
|
|
||||||
`src/core/persistence.js` handles:
|
`src/core/persistence.js` handles:
|
||||||
@@ -187,20 +224,35 @@ All extension state lives in `src/core/state.js` as module-level `let` variables
|
|||||||
- Settings versioning: automatic migration via `CURRENT_SETTINGS_VERSION` (currently 7)
|
- Settings versioning: automatic migration via `CURRENT_SETTINGS_VERSION` (currently 7)
|
||||||
- Deferred saves: batches chat data saves to avoid excessive writes
|
- Deferred saves: batches chat data saves to avoid excessive writes
|
||||||
|
|
||||||
|
### Memory management
|
||||||
|
|
||||||
|
- `clearDomCache()`: Invalidates jQuery object cache on panel rebuild
|
||||||
|
- Event tracking: All `on()`/`once()` handlers tracked for cleanup via `unregisterAllEvents()`
|
||||||
|
- `clearDebugLogs()`: Called on `CHAT_CHANGED` to prevent unbounded debug log growth
|
||||||
|
|
||||||
|
### Performance optimizations
|
||||||
|
|
||||||
|
- **Rendering**: `requestAnimationFrame` batching in `rerenderRpgState()`, `updateIfChanged()` cache comparison in `renderUtils.js`, CSS `content-visibility: auto` on inventory/equipment containers
|
||||||
|
- **Parser**: ~64 regex patterns pre-compiled as module-level constants, format detection caching with `lastDetectedFormat` (3-hit threshold)
|
||||||
|
- **CSS**: Minification via `--minify` flag, extended CSS custom properties, theme refactoring (379 → ~220 lines)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Development commands
|
## Development commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Validate translation files (check missing keys, type mismatches, empty values)
|
# Build CSS from modular sources
|
||||||
npm run validate_locale_once
|
npm run build:css # Concatenate CSS modules into style.css
|
||||||
|
npm run build:css:min # Minified production build (~50% smaller)
|
||||||
|
|
||||||
# Watch mode: re-validate on file changes
|
# Testing
|
||||||
npm run validate_locale
|
npm test # Run Jest test suite (78 tests)
|
||||||
|
|
||||||
|
# Translation validation
|
||||||
|
npm run validate_locale # Watch mode: re-validate on file changes
|
||||||
|
npm run validate_locale_once # Single validation run
|
||||||
```
|
```
|
||||||
|
|
||||||
No build, lint, test, or typecheck commands exist. The code runs directly in the browser.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## i18n system
|
## i18n system
|
||||||
@@ -256,6 +308,14 @@ All CSS classes use `rpg-` prefix to avoid conflicts with SillyTavern and other
|
|||||||
- `.rpg-panel`, `.rpg-section`, `.rpg-stat-bar`, `.rpg-btn-primary`, etc.
|
- `.rpg-panel`, `.rpg-section`, `.rpg-stat-bar`, `.rpg-btn-primary`, etc.
|
||||||
- CSS custom properties: `--rpg-bg`, `--rpg-accent`, `--rpg-text`, `--rpg-highlight`, `--rpg-border`, `--rpg-shadow`
|
- CSS custom properties: `--rpg-bg`, `--rpg-accent`, `--rpg-text`, `--rpg-highlight`, `--rpg-border`, `--rpg-shadow`
|
||||||
|
|
||||||
|
### CSS build pipeline
|
||||||
|
|
||||||
|
CSS modules in `src/styles/` are concatenated in numeric order by `scripts/build-css.js`. Modules follow the naming convention `_NN_name.css` where NN is the load order. After modifying any CSS module, run `npm run build:css` to regenerate `style.css`.
|
||||||
|
|
||||||
|
### Validator factory
|
||||||
|
|
||||||
|
Input sanitization uses `createStringValidator()` from `src/utils/security.js` — a factory pattern that unifies `sanitizeLocationName`/`sanitizeItemName` with configurable rules (max length, allowed characters, etc.).
|
||||||
|
|
||||||
### Panel structure
|
### Panel structure
|
||||||
|
|
||||||
The main panel (`template.html`) has these sections in order:
|
The main panel (`template.html`) has these sections in order:
|
||||||
@@ -290,7 +350,7 @@ The `skipInjectionsForGuided` setting controls tracker injection behavior during
|
|||||||
|
|
||||||
2. **Relative import depth matters**: Files in `src/` use deeper relative paths (`../../../../../../script.js`) than `index.js` (`../../../../script.js`). When moving files between directories, adjust accordingly.
|
2. **Relative import depth matters**: Files in `src/` use deeper relative paths (`../../../../../../script.js`) than `index.js` (`../../../../script.js`). When moving files between directories, adjust accordingly.
|
||||||
|
|
||||||
3. **Settings merge**: On load, saved settings are deep-merged with defaults from `src/core/state.js`. New settings fields should be added to both the state module (runtime defaults) AND `src/core/config.js` (documentation defaults).
|
3. **Settings merge**: On load, saved settings are deep-merged with defaults. `config.js` is the single source of truth for default settings — `state.js` imports from it. New settings fields should be added to `config.js`.
|
||||||
|
|
||||||
4. **Chat metadata**: Per-chat tracker data lives in `chat_metadata.rpg_companion`. Per-swipe data lives on individual chat message objects in the `swipe_store`.
|
4. **Chat metadata**: Per-chat tracker data lives in `chat_metadata.rpg_companion`. Per-swipe data lives on individual chat message objects in the `swipe_store`.
|
||||||
|
|
||||||
@@ -302,9 +362,13 @@ The `skipInjectionsForGuided` setting controls tracker injection behavior during
|
|||||||
|
|
||||||
8. **External API key**: Stored in `localStorage` as `rpg_companion_external_api_key`, NOT in extension settings (for security).
|
8. **External API key**: Stored in `localStorage` as `rpg_companion_external_api_key`, NOT in extension settings (for security).
|
||||||
|
|
||||||
9. **The `package.json` has no runtime dependencies**. The `devDependencies` (chokidar, fs-extra, glob) are only for the i18n validator.
|
9. **Dev dependencies**: `package.json` has devDependencies (Jest, Babel, chokidar, fs-extra, glob, execa) for testing and i18n validation. No runtime dependencies — the extension runs entirely in the browser.
|
||||||
|
|
||||||
10. **Deprecation status**: The README states this extension is deprecated in favor of Marinara Engine. The code shows a deprecation modal that displays on first load.
|
10. **CSS rebuild required**: After modifying any file in `src/styles/`, run `npm run build:css` to regenerate `style.css`. SillyTavern loads `style.css`, not the individual modules.
|
||||||
|
|
||||||
|
11. **Deprecation status**: The README states this extension is deprecated in favor of Marinara Engine. The code shows a deprecation modal that displays on first load.
|
||||||
|
|
||||||
|
12. **ESM testing**: Jest uses Babel for ESM transformation (`babel.config.json` + `jest.config.cjs`). All test files use ES module syntax.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -312,19 +376,25 @@ The `skipInjectionsForGuided` setting controls tracker injection behavior during
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `index.js` | Main entry point. All imports, UI init, event registration, startup logic. |
|
| `index.js` | Main entry point. Imports, UI init, event registration, startup logic (~456 lines after refactor). |
|
||||||
| `manifest.json` | SillyTavern extension manifest (name, loading order, JS/CSS files). |
|
| `manifest.json` | SillyTavern extension manifest (name, loading order, JS/CSS files). |
|
||||||
| `style.css` | All CSS (~12,300 lines). Themes, animations, responsive breakpoints. |
|
| `style.css` | Built CSS output (concatenated from `src/styles/` modules). Do not edit directly. |
|
||||||
| `template.html` | Main panel HTML template. Rendered by SillyTavern's `renderExtensionTemplateAsync`. |
|
| `template.html` | Main panel HTML template. Rendered by SillyTavern's `renderExtensionTemplateAsync`. |
|
||||||
| `settings.html` | Extensions tab settings drawer. Minimal — most settings are in the panel modal. |
|
| `settings.html` | Extensions tab settings drawer. Minimal — most settings are in the panel modal. |
|
||||||
| `src/core/state.js` | Central state module. All extension-wide variables live here. |
|
| `scripts/build-css.js` | CSS build script. Concatenates modules, supports `--minify` flag. |
|
||||||
|
| `src/core/state.js` | Central state module. Imports defaults from config.js. All extension-wide variables. |
|
||||||
| `src/core/persistence.js` | Settings/chat data save/load, version migrations. |
|
| `src/core/persistence.js` | Settings/chat data save/load, version migrations. |
|
||||||
| `src/core/config.js` | Extension name, path detection, default settings documentation. |
|
| `src/core/config.js` | Extension name, path detection, default settings (single source of truth). |
|
||||||
| `src/core/events.js` | SillyTavern event system wrapper with bulk register/unregister. |
|
| `src/core/events.js` | SillyTavern event system wrapper with bulk register/unregister. |
|
||||||
| `src/core/i18n.js` | Translation loader and DOM applier. |
|
| `src/core/i18n.js` | Translation loader and DOM applier. |
|
||||||
|
| `src/core/settingsPanel.js` | Extracted settings panel UI (from index.js refactor, PR #6). |
|
||||||
| `src/systems/integration/sillytavern.js` | All SillyTavern event handlers (message lifecycle, swipes, chat changes). |
|
| `src/systems/integration/sillytavern.js` | All SillyTavern event handlers (message lifecycle, swipes, chat changes). |
|
||||||
| `src/systems/generation/promptBuilder.js` | Builds AI prompts for tracker generation. |
|
| `src/systems/generation/promptBuilder.js` | Builds AI prompts for tracker generation (refactored with focused helpers). |
|
||||||
| `src/systems/generation/parser.js` | Parses AI responses to extract tracker JSON. |
|
| `src/systems/generation/parser.js` | Parses AI responses to extract tracker JSON (~50 pre-compiled regex patterns). |
|
||||||
| `src/systems/generation/apiClient.js` | API client for separate/external generation modes. |
|
| `src/systems/generation/apiClient.js` | API client for separate/external generation modes. |
|
||||||
| `src/systems/generation/injector.js` | Injects tracker prompts into SillyTavern's generation context. |
|
| `src/systems/generation/injector.js` | Injects tracker prompts into SillyTavern's generation context. |
|
||||||
|
| `src/systems/rendering/renderUtils.js` | Rendering utilities: `updateIfChanged()` cache, rAF batching. |
|
||||||
|
| `src/systems/equipment/constants.js` | Equipment slot definitions, type limits, stat configuration. |
|
||||||
|
| `src/utils/security.js` | Input validation, sanitization, validator factory (`createStringValidator()`). |
|
||||||
|
| `src/utils/jsonRepair.js` | JSON repair for malformed AI responses (14 pre-compiled regex patterns). |
|
||||||
| `src/i18n/validator.js` | Node.js script for validating translation consistency. |
|
| `src/i18n/validator.js` | Node.js script for validating translation consistency. |
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Branch Cleanup — July 13, 2026
|
||||||
|
|
||||||
|
The following stale remote branches were deleted, leaving only `main`:
|
||||||
|
|
||||||
|
- `SpicyMarinara-patch-1`
|
||||||
|
- `feature/equipment-system`
|
||||||
|
- `pr-109`
|
||||||
|
- `revert-111-main`
|
||||||
|
- `revert-116-revert-111-main`
|
||||||
|
- `revert-36-feat/v2-widget-dashboard-system`
|
||||||
|
- `revert-40-feat/responsive-dashboard-layout`
|
||||||
|
- `revert-59-main`
|
||||||
|
- `test-pr90-pr91-combined`
|
||||||
@@ -2,16 +2,8 @@
|
|||||||
|
|
||||||
An immersive RPG extension for browsers that tracks character stats, scene information, and character thoughts in a beautiful, customizable UI panel. All automated! Works with any preset. Choose between Together or Separate generation modes for context and generation control.
|
An immersive RPG extension for browsers that tracks character stats, scene information, and character thoughts in a beautiful, customizable UI panel. All automated! Works with any preset. Choose between Together or Separate generation modes for context and generation control.
|
||||||
|
|
||||||
[](https://discord.com/invite/KdAkTg94ME)
|
[](https://discord.com/invite/KdAkTg94ME)
|
||||||
[](https://ko-fi.com/marinara_spaghetti)
|
[](https://ko-fi.com/marinara_spaghetti)
|
||||||
|
|
||||||
## 🆕 What's New
|
|
||||||
|
|
||||||
### DEPRECATED
|
|
||||||
|
|
||||||
Moving on to developing the Marinara Engine frontend, the extension will now be maintained by the community!
|
|
||||||
|
|
||||||
<https://github.com/Pasta-Devs/Marinara-Engine>
|
|
||||||
|
|
||||||
## 📥 Installation
|
## 📥 Installation
|
||||||
|
|
||||||
@@ -21,7 +13,7 @@ Moving on to developing the Marinara Engine frontend, the extension will now be
|
|||||||
|
|
||||||
3. Go to Install extension
|
3. Go to Install extension
|
||||||
|
|
||||||
4. Copy-paste this link: <https://github.com/SpicyMarinara/rpg-companion-sillytavern>
|
4. Copy-paste this link: <https://gitea.zephyre.one/Pakobbix/rpg-companion-sillytavern>
|
||||||
|
|
||||||
5. Press Install for all users/Install just for me
|
5. Press Install for all users/Install just for me
|
||||||
|
|
||||||
@@ -41,6 +33,7 @@ Moving on to developing the Marinara Engine frontend, the extension will now be
|
|||||||
- **🎭 Floating Thought Bubbles**: Optional thought bubbles positioned next to character avatars in chat
|
- **🎭 Floating Thought Bubbles**: Optional thought bubbles positioned next to character avatars in chat
|
||||||
- **🎲 Classic RPG Stats**: STR, DEX, CON, INT, WIS, CHA attributes with dice roll support
|
- **🎲 Classic RPG Stats**: STR, DEX, CON, INT, WIS, CHA attributes with dice roll support
|
||||||
- **📦 Advanced Inventory System**: Multi-location storage (On Person, Stored locations, Assets) with v2 format
|
- **📦 Advanced Inventory System**: Multi-location storage (On Person, Stored locations, Assets) with v2 format
|
||||||
|
- **⚔️ Equipment System**: 19 equipment slots (Helmet, Necklace, Body Armor, Gloves, Pants, Shoes, 10 Rings, 3 Accessories) with slot-type validation, stat bonuses (+99 max), and automatic slot assignment
|
||||||
- **🎯 Character Stats**: Track health, energy, or any custom stats for each present character with color interpolation
|
- **🎯 Character Stats**: Track health, energy, or any custom stats for each present character with color interpolation
|
||||||
- **📜 Immersive HTML**: Enhance the immersion by including creative HTML/CSS/JS elements in your roleplay
|
- **📜 Immersive HTML**: Enhance the immersion by including creative HTML/CSS/JS elements in your roleplay
|
||||||
- **➡️ Plot Progression**: Progress the plot with randomized events or natural progression with a click of a button
|
- **➡️ Plot Progression**: Progress the plot with randomized events or natural progression with a click of a button
|
||||||
@@ -226,6 +219,49 @@ Choose from 6 beautiful themes:
|
|||||||
|
|
||||||
If you ever have an awesome idea to do your own SillyTavern extension, don't.
|
If you ever have an awesome idea to do your own SillyTavern extension, don't.
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- **index.js refactored**: 1567 lines → 456 lines (73% reduction) through modular extraction
|
||||||
|
- **CSS modularized**: 28 logical modules in `src/styles/` with automated build script
|
||||||
|
- **Settings consolidation**: `config.js` as single source of truth — state.js imports from it (~300 lines eliminated)
|
||||||
|
- **Validator factory pattern**: `createStringValidator()` unifies `sanitizeLocationName`/`sanitizeItemName`
|
||||||
|
- **Prompt builder refactored**: `getCharacterCardsInfo` extracted into focused helper functions
|
||||||
|
- **Error visibility**: `jsonRepair.js` silent failures now log to `console.debug`
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **Rendering**: `requestAnimationFrame` batching in `rerenderRpgState()`, `updateIfChanged()` cache in `renderUtils.js`, CSS `content-visibility: auto` on inventory/equipment containers
|
||||||
|
- **Parser**: Regex pre-compilation (~64 patterns in `parser.js` + `jsonRepair.js` as module-level constants), format detection caching (`lastDetectedFormat` with 3-hit threshold), `clearFormatCache()` for invalidation
|
||||||
|
- **CSS**: Minification support (`--minify` flag in build script), extended CSS custom properties, theme refactoring (379 → ~220 lines), `style.css` reduced from 305.4 KB → 293.9 KB (3.8%), minified build adds ~50%+ further reduction
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
|
||||||
|
- **`clearDomCache()`**: Invalidates jQuery object cache on panel rebuild to prevent stale references
|
||||||
|
- **Event tracking**: All `on()`/`once()` handlers tracked for cleanup via `unregisterAllEvents()`
|
||||||
|
- **`clearDebugLogs()`**: Called on `CHAT_CHANGED` to prevent unbounded debug log growth
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- **78 tests passing** (up from 54) using Jest with Babel ESM support
|
||||||
|
- ESM compatibility via `babel.config.json` + `jest.config.cjs` transform configuration
|
||||||
|
- Security-focused test suite (`security.test.js`, 24 tests)
|
||||||
|
|
||||||
|
### Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:css # Concatenate CSS modules
|
||||||
|
npm run build:css:min # Minified production build
|
||||||
|
npm test # Run Jest test suite
|
||||||
|
npm run validate_locale # Watch mode translation validation
|
||||||
|
npm run validate_locale_once
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
|
||||||
|
- `"type": "module"` — ES modules throughout
|
||||||
|
- Jest + Babel for testing with full ESM support
|
||||||
|
- Modular CSS build pipeline with minification support
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
### Extension doesn't appear
|
### Extension doesn't appear
|
||||||
@@ -290,7 +326,4 @@ SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDea
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Made with ❤️ by Marinara
|
Made with ❤️ by Marinara
|
||||||
|
|
||||||
PS I'm looking for a job or a sponsor to fund my custom AI frontend, contact me if interested:
|
|
||||||
[mgrabower97@gmail.com](mailto:mgrabower97@gmail.com)
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const saveSettings = jest.fn();
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Tests for CSS build script (scripts/build-css.js)
|
||||||
|
* Tests minification logic and build output.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const rootDir = path.resolve(__dirname, '..');
|
||||||
|
const stylesDir = path.join(rootDir, 'src', 'styles');
|
||||||
|
const outputFile = path.join(rootDir, 'style.css');
|
||||||
|
|
||||||
|
describe('CSS Build Script', () => {
|
||||||
|
let originalContent;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// Save original style.css for restoration
|
||||||
|
if (await fs.pathExists(outputFile)) {
|
||||||
|
originalContent = await fs.readFile(outputFile, 'utf-8');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Restore original style.css
|
||||||
|
if (originalContent) {
|
||||||
|
await fs.writeFile(outputFile, originalContent, 'utf-8');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('styles directory contains CSS files', async () => {
|
||||||
|
const files = await fs.readdir(stylesDir);
|
||||||
|
const cssFiles = files.filter(f => f.endsWith('.css'));
|
||||||
|
expect(cssFiles.length).toBeGreaterThan(0);
|
||||||
|
expect(cssFiles).toContain('_variables.css');
|
||||||
|
expect(cssFiles).toContain('_08_themes.css');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('build:css produces readable output with comments', () => {
|
||||||
|
const result = execSync('node scripts/build-css.js', { cwd: rootDir, encoding: 'utf-8' });
|
||||||
|
|
||||||
|
expect(result).toContain('Building CSS from src/styles/');
|
||||||
|
|
||||||
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
||||||
|
// Readable build should contain section comments
|
||||||
|
expect(content).toContain('/* ============================================ */');
|
||||||
|
expect(content).toContain('/* _variables.css */');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('build:css --minify produces minified output', () => {
|
||||||
|
const result = execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
||||||
|
|
||||||
|
expect(result).toContain('minified');
|
||||||
|
expect(result).toContain('Minification:');
|
||||||
|
|
||||||
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
||||||
|
// Minified build should NOT contain section comments
|
||||||
|
expect(content).not.toContain('/* ============================================ */');
|
||||||
|
// Should not have multiple consecutive spaces (except in strings)
|
||||||
|
expect(content).not.toMatch(/[^\S\r\n]{2,}/);
|
||||||
|
// Should not have newlines between rules
|
||||||
|
const lines = content.split('\n');
|
||||||
|
expect(lines.length).toBeLessThan(1000); // Original is ~12000 lines
|
||||||
|
});
|
||||||
|
|
||||||
|
test('minified output is smaller than readable output', () => {
|
||||||
|
// Build readable
|
||||||
|
execSync('node scripts/build-css.js', { cwd: rootDir, encoding: 'utf-8' });
|
||||||
|
const readableStats = fs.statSync(outputFile);
|
||||||
|
const readableSize = readableStats.size;
|
||||||
|
|
||||||
|
// Build minified
|
||||||
|
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
||||||
|
const minifiedStats = fs.statSync(outputFile);
|
||||||
|
const minifiedSize = minifiedStats.size;
|
||||||
|
|
||||||
|
expect(minifiedSize).toBeLessThan(readableSize);
|
||||||
|
// Expect at least 30% reduction (comments + whitespace)
|
||||||
|
const reduction = ((readableSize - minifiedSize) / readableSize) * 100;
|
||||||
|
expect(reduction).toBeGreaterThan(30);
|
||||||
|
|
||||||
|
// Restore readable version for other tests
|
||||||
|
execSync('node scripts/build-css.js', { cwd: rootDir, encoding: 'utf-8' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('minified output preserves CSS custom properties', () => {
|
||||||
|
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
||||||
|
|
||||||
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
||||||
|
// CSS variables should be preserved
|
||||||
|
expect(content).toContain('--rpg-bg');
|
||||||
|
expect(content).toContain('--rpg-accent');
|
||||||
|
expect(content).toContain('--rpg-text');
|
||||||
|
expect(content).toContain('--rpg-highlight');
|
||||||
|
expect(content).toContain('--rpg-border');
|
||||||
|
expect(content).toContain('--rpg-shadow');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('minified output preserves data URIs', () => {
|
||||||
|
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
||||||
|
|
||||||
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
||||||
|
// Fantasy theme has SVG data URI — should be preserved
|
||||||
|
expect(content).toContain('data:image/svg+xml');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('minified output preserves data-theme selectors', () => {
|
||||||
|
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
||||||
|
|
||||||
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
||||||
|
expect(content).toContain('[data-theme="sci-fi"]');
|
||||||
|
expect(content).toContain('[data-theme="fantasy"]');
|
||||||
|
expect(content).toContain('[data-theme="cyberpunk"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('themes.css uses grouped [data-theme] selectors (no duplication)', async () => {
|
||||||
|
const themesContent = await fs.readFile(path.join(stylesDir, '_08_themes.css'), 'utf-8');
|
||||||
|
|
||||||
|
// Should use [data-theme] grouped selectors instead of repeating all 3 themes
|
||||||
|
expect(themesContent).toContain('.rpg-panel[data-theme] .rpg-tabs-nav');
|
||||||
|
expect(themesContent).toContain('.rpg-panel[data-theme] .rpg-tab-btn');
|
||||||
|
|
||||||
|
// Should NOT have the old pattern of listing all 3 themes for every element
|
||||||
|
const oldPattern = /\.rpg-panel\[data-theme="sci-fi"\] \.rpg-tabs-nav,\s*\.rpg-panel\[data-theme="fantasy"\] \.rpg-tabs-nav,\s*\.rpg-panel\[data-theme="cyberpunk"\] \.rpg-tabs-nav/;
|
||||||
|
expect(themesContent).not.toMatch(oldPattern);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('themes.css defines extended custom properties per theme', async () => {
|
||||||
|
const themesContent = await fs.readFile(path.join(stylesDir, '_08_themes.css'), 'utf-8');
|
||||||
|
|
||||||
|
// Extended properties for content box
|
||||||
|
expect(themesContent).toContain('--rpg-content-bg');
|
||||||
|
expect(themesContent).toContain('--rpg-content-box-shadow');
|
||||||
|
expect(themesContent).toContain('--rpg-content-border');
|
||||||
|
|
||||||
|
// Extended properties for headers
|
||||||
|
expect(themesContent).toContain('--rpg-header-text-shadow');
|
||||||
|
expect(themesContent).toContain('--rpg-header-font-family');
|
||||||
|
|
||||||
|
// Extended properties for dividers
|
||||||
|
expect(themesContent).toContain('--rpg-divider-bg');
|
||||||
|
expect(themesContent).toContain('--rpg-divider-content');
|
||||||
|
|
||||||
|
// Extended properties for thoughts
|
||||||
|
expect(themesContent).toContain('--rpg-thoughts-bg');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { repairJSON, extractJSONFromText, validateJSONSchema, safeParseJSON } from '../src/utils/jsonRepair.js';
|
||||||
|
|
||||||
|
describe('repairJSON', () => {
|
||||||
|
test('parses valid JSON', () => {
|
||||||
|
const input = '{"name": "test", "value": 42}';
|
||||||
|
const result = repairJSON(input);
|
||||||
|
expect(result).toEqual({ name: 'test', value: 42 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles trailing commas', () => {
|
||||||
|
const input = '{"name": "test", "value": 42,}';
|
||||||
|
const result = repairJSON(input);
|
||||||
|
expect(result).toEqual({ name: 'test', value: 42 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles markdown code fences', () => {
|
||||||
|
const input = '```json\n{"name": "test"}\n```';
|
||||||
|
const result = repairJSON(input);
|
||||||
|
expect(result).toEqual({ name: 'test' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes thinking tags', () => {
|
||||||
|
const input = '```json\n{"name": "test"}\n```';
|
||||||
|
const result = repairJSON(input);
|
||||||
|
expect(result).toEqual({ name: 'test' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for invalid input', () => {
|
||||||
|
expect(repairJSON(null)).toBeNull();
|
||||||
|
expect(repairJSON(123)).toBeNull();
|
||||||
|
expect(repairJSON('')).toBeNull();
|
||||||
|
expect(repairJSON('not json at all')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles JSON arrays', () => {
|
||||||
|
const input = '[1, 2, 3]';
|
||||||
|
const result = repairJSON(input);
|
||||||
|
expect(result).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles nested objects', () => {
|
||||||
|
const input = '{"outer": {"inner": {"deep": true}}}';
|
||||||
|
const result = repairJSON(input);
|
||||||
|
expect(result).toEqual({ outer: { inner: { deep: true } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes JavaScript comments', () => {
|
||||||
|
const input = '{"name": "test"} // comment';
|
||||||
|
const result = repairJSON(input);
|
||||||
|
expect(result).toEqual({ name: 'test' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractJSONFromText', () => {
|
||||||
|
test('extracts from json code fence', () => {
|
||||||
|
const input = 'Here is some text\n```json\n{"key": "value"}\n```\nMore text';
|
||||||
|
const result = extractJSONFromText(input);
|
||||||
|
expect(result).toBe('{"key": "value"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extracts from generic code fence', () => {
|
||||||
|
const input = '```{"key": "value"}```';
|
||||||
|
const result = extractJSONFromText(input);
|
||||||
|
expect(result).toBe('{"key": "value"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extracts standalone JSON object', () => {
|
||||||
|
const input = 'Some text {"key": "value"} more text';
|
||||||
|
const result = extractJSONFromText(input);
|
||||||
|
expect(result).toBe('{"key": "value"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extracts standalone JSON array', () => {
|
||||||
|
const input = 'Some text [1, 2, 3] more text';
|
||||||
|
const result = extractJSONFromText(input);
|
||||||
|
expect(result).toBe('[1, 2, 3]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for no JSON', () => {
|
||||||
|
expect(extractJSONFromText('no json here')).toBeNull();
|
||||||
|
expect(extractJSONFromText(null)).toBeNull();
|
||||||
|
expect(extractJSONFromText(123)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateJSONSchema', () => {
|
||||||
|
test('validates userStats schema', () => {
|
||||||
|
const valid = { stats: [{ id: 'health', name: 'Health', value: 100 }] };
|
||||||
|
const invalid = { stats: [{ id: 'health', name: 'Health' }] }; // missing value
|
||||||
|
expect(validateJSONSchema(valid, 'userStats')).toBe(true);
|
||||||
|
expect(validateJSONSchema(invalid, 'userStats')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates infoBox schema', () => {
|
||||||
|
const valid = { date: '2024-01-01' };
|
||||||
|
const invalid = { unrelated: 'data' };
|
||||||
|
// Note: validateJSONSchema returns the truthy field value, not strictly true
|
||||||
|
expect(validateJSONSchema(valid, 'infoBox')).toBeTruthy();
|
||||||
|
expect(validateJSONSchema(invalid, 'infoBox')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates characters schema', () => {
|
||||||
|
const valid = { characters: [{ name: 'Alice' }] };
|
||||||
|
const invalid = { characters: [{ noName: true }] };
|
||||||
|
expect(validateJSONSchema(valid, 'characters')).toBe(true);
|
||||||
|
expect(validateJSONSchema(invalid, 'characters')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for invalid types', () => {
|
||||||
|
expect(validateJSONSchema(null, 'userStats')).toBe(false);
|
||||||
|
expect(validateJSONSchema('string', 'userStats')).toBe(false);
|
||||||
|
expect(validateJSONSchema({}, 'unknown')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('safeParseJSON', () => {
|
||||||
|
test('successfully parses and validates', () => {
|
||||||
|
const result = safeParseJSON('{"stats": [{"id": "health", "name": "Health", "value": 100}]}', 'userStats');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data).toEqual({ stats: [{ id: 'health', name: 'Health', value: 100 }] });
|
||||||
|
expect(result.error).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns error when no JSON found', () => {
|
||||||
|
const result = safeParseJSON('no json here');
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('No JSON found in text');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns error when schema validation fails', () => {
|
||||||
|
const result = safeParseJSON('{"unrelated": "data"}', 'userStats');
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toContain('does not match expected schema');
|
||||||
|
expect(result.data).not.toBeNull(); // Still returns data
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* Memory Management Tests
|
||||||
|
* Tests DOM cache invalidation, event listener cleanup, and debug log management
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('DOM Cache Invalidation', () => {
|
||||||
|
test('clearDomCache sets all jQuery references to null', () => {
|
||||||
|
// Simulate the clearDomCache logic
|
||||||
|
const domCache = {
|
||||||
|
$panelContainer: { isPanel: true },
|
||||||
|
$userStatsContainer: { isStats: true },
|
||||||
|
$infoBoxContainer: { isInfoBox: true },
|
||||||
|
$thoughtsContainer: { isThoughts: true },
|
||||||
|
$inventoryContainer: { isInventory: true },
|
||||||
|
$questsContainer: { isQuests: true },
|
||||||
|
$musicPlayerContainer: { isMusic: true },
|
||||||
|
$equipmentContainer: { isEquipment: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulate clearDomCache
|
||||||
|
domCache.$panelContainer = null;
|
||||||
|
domCache.$userStatsContainer = null;
|
||||||
|
domCache.$infoBoxContainer = null;
|
||||||
|
domCache.$thoughtsContainer = null;
|
||||||
|
domCache.$inventoryContainer = null;
|
||||||
|
domCache.$questsContainer = null;
|
||||||
|
domCache.$musicPlayerContainer = null;
|
||||||
|
domCache.$equipmentContainer = null;
|
||||||
|
|
||||||
|
// Verify all are null
|
||||||
|
expect(domCache.$panelContainer).toBeNull();
|
||||||
|
expect(domCache.$userStatsContainer).toBeNull();
|
||||||
|
expect(domCache.$infoBoxContainer).toBeNull();
|
||||||
|
expect(domCache.$thoughtsContainer).toBeNull();
|
||||||
|
expect(domCache.$inventoryContainer).toBeNull();
|
||||||
|
expect(domCache.$questsContainer).toBeNull();
|
||||||
|
expect(domCache.$musicPlayerContainer).toBeNull();
|
||||||
|
expect(domCache.$equipmentContainer).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearDomCache is idempotent (safe to call multiple times)', () => {
|
||||||
|
const cache = { $panelContainer: null };
|
||||||
|
|
||||||
|
// Call clear twice - should not throw
|
||||||
|
cache.$panelContainer = null;
|
||||||
|
cache.$panelContainer = null;
|
||||||
|
|
||||||
|
expect(cache.$panelContainer).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Debug Log Memory Management', () => {
|
||||||
|
test('debugLogs array is capped at 100 entries', () => {
|
||||||
|
const debugLogs = [];
|
||||||
|
const MAX_LOGS = 100;
|
||||||
|
|
||||||
|
// Add 150 entries
|
||||||
|
for (let i = 0; i < 150; i++) {
|
||||||
|
debugLogs.push({ timestamp: new Date().toISOString(), message: `Log ${i}` });
|
||||||
|
if (debugLogs.length > MAX_LOGS) {
|
||||||
|
debugLogs.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(debugLogs.length).toBe(100);
|
||||||
|
expect(debugLogs[0].message).toBe('Log 50'); // First 50 were shifted out
|
||||||
|
expect(debugLogs[99].message).toBe('Log 149');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearDebugLogs empties the array', () => {
|
||||||
|
const debugLogs = [
|
||||||
|
{ timestamp: '2024-01-01T00:00:00Z', message: 'Log 1' },
|
||||||
|
{ timestamp: '2024-01-01T00:00:01Z', message: 'Log 2' },
|
||||||
|
{ timestamp: '2024-01-01T00:00:02Z', message: 'Log 3' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Simulate clearDebugLogs: debugLogs.length = 0
|
||||||
|
debugLogs.length = 0;
|
||||||
|
|
||||||
|
expect(debugLogs.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('debugLogs cleared on chat change prevents memory accumulation', () => {
|
||||||
|
const debugLogs = [];
|
||||||
|
const MAX_LOGS = 100;
|
||||||
|
|
||||||
|
// Simulate multiple chat sessions
|
||||||
|
for (let chat = 0; chat < 5; chat++) {
|
||||||
|
// Clear on chat change
|
||||||
|
debugLogs.length = 0;
|
||||||
|
|
||||||
|
// Add logs during this chat session
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
debugLogs.push({ timestamp: new Date().toISOString(), message: `Chat ${chat} Log ${i}` });
|
||||||
|
if (debugLogs.length > MAX_LOGS) {
|
||||||
|
debugLogs.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After 5 chats with clearing, we should only have logs from the last chat
|
||||||
|
expect(debugLogs.length).toBe(30);
|
||||||
|
expect(debugLogs[0].message).toBe('Chat 4 Log 0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Event Listener Cleanup', () => {
|
||||||
|
test('registered handlers are tracked for cleanup', () => {
|
||||||
|
// Simulate the registeredHandlers Map from events.js
|
||||||
|
const registeredHandlers = new Map();
|
||||||
|
|
||||||
|
function trackHandler(eventType, handler) {
|
||||||
|
if (!registeredHandlers.has(eventType)) {
|
||||||
|
registeredHandlers.set(eventType, []);
|
||||||
|
}
|
||||||
|
registeredHandlers.get(eventType).push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler1 = () => {};
|
||||||
|
const handler2 = () => {};
|
||||||
|
|
||||||
|
trackHandler('MESSAGE_SENT', handler1);
|
||||||
|
trackHandler('MESSAGE_SENT', handler2);
|
||||||
|
trackHandler('CHAT_CHANGED', handler1);
|
||||||
|
|
||||||
|
expect(registeredHandlers.get('MESSAGE_SENT').length).toBe(2);
|
||||||
|
expect(registeredHandlers.get('CHAT_CHANGED').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unregisterAllEvents clears all tracked handlers', () => {
|
||||||
|
const registeredHandlers = new Map();
|
||||||
|
|
||||||
|
function trackHandler(eventType, handler) {
|
||||||
|
if (!registeredHandlers.has(eventType)) {
|
||||||
|
registeredHandlers.set(eventType, []);
|
||||||
|
}
|
||||||
|
registeredHandlers.get(eventType).push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
trackHandler('MESSAGE_SENT', () => {});
|
||||||
|
trackHandler('CHAT_CHANGED', () => {});
|
||||||
|
|
||||||
|
// Simulate unregisterAllEvents
|
||||||
|
registeredHandlers.clear();
|
||||||
|
|
||||||
|
expect(registeredHandlers.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('on() function tracks handlers for cleanup', () => {
|
||||||
|
const registeredHandlers = new Map();
|
||||||
|
|
||||||
|
// Simulate the on() function with tracking
|
||||||
|
function on(eventType, handler) {
|
||||||
|
// eventSource.on(eventType, handler); // Would call real event source
|
||||||
|
if (!registeredHandlers.has(eventType)) {
|
||||||
|
registeredHandlers.set(eventType, []);
|
||||||
|
}
|
||||||
|
registeredHandlers.get(eventType).push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = () => {};
|
||||||
|
on('GENERATE_BEFORE_COMBINE_PROMPTS', handler);
|
||||||
|
|
||||||
|
expect(registeredHandlers.has('GENERATE_BEFORE_COMBINE_PROMPTS')).toBe(true);
|
||||||
|
expect(registeredHandlers.get('GENERATE_BEFORE_COMBINE_PROMPTS').includes(handler)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('once() function tracks handlers for cleanup', () => {
|
||||||
|
const registeredHandlers = new Map();
|
||||||
|
|
||||||
|
// Simulate the once() function with tracking
|
||||||
|
function once(eventType, handler) {
|
||||||
|
// eventSource.once(eventType, handler); // Would call real event source
|
||||||
|
if (!registeredHandlers.has(eventType)) {
|
||||||
|
registeredHandlers.set(eventType, []);
|
||||||
|
}
|
||||||
|
registeredHandlers.get(eventType).push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = () => {};
|
||||||
|
once('TEXT_COMPLETION_SETTINGS_READY', handler);
|
||||||
|
|
||||||
|
expect(registeredHandlers.has('TEXT_COMPLETION_SETTINGS_READY')).toBe(true);
|
||||||
|
expect(registeredHandlers.get('TEXT_COMPLETION_SETTINGS_READY').includes(handler)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiple handlers for same event type are all tracked', () => {
|
||||||
|
const registeredHandlers = new Map();
|
||||||
|
|
||||||
|
function on(eventType, handler) {
|
||||||
|
if (!registeredHandlers.has(eventType)) {
|
||||||
|
registeredHandlers.set(eventType, []);
|
||||||
|
}
|
||||||
|
registeredHandlers.get(eventType).push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlers = [() => {}, () => {}, () => {}];
|
||||||
|
handlers.forEach(h => on('CHAT_CHANGED', h));
|
||||||
|
|
||||||
|
expect(registeredHandlers.get('CHAT_CHANGED').length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('jQuery Event Handler Tracking', () => {
|
||||||
|
test('tracked jQuery handlers can be cleaned up', () => {
|
||||||
|
const jqueryEventHandlers = [];
|
||||||
|
|
||||||
|
function trackJQueryHandler(event, selector, handler) {
|
||||||
|
jqueryEventHandlers.push({ event, selector, handler });
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler1 = () => {};
|
||||||
|
const handler2 = () => {};
|
||||||
|
|
||||||
|
trackJQueryHandler('click', '.rpg-item-remove', handler1);
|
||||||
|
trackJQueryHandler('click', '.rpg-equip-btn', handler2);
|
||||||
|
|
||||||
|
expect(jqueryEventHandlers.length).toBe(2);
|
||||||
|
|
||||||
|
// Simulate cleanup
|
||||||
|
jqueryEventHandlers.length = 0;
|
||||||
|
expect(jqueryEventHandlers.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trackJQueryHandler returns the handler for chaining', () => {
|
||||||
|
const jqueryEventHandlers = [];
|
||||||
|
|
||||||
|
function trackJQueryHandler(event, selector, handler) {
|
||||||
|
jqueryEventHandlers.push({ event, selector, handler });
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = () => 'test';
|
||||||
|
const returned = trackJQueryHandler('click', '.selector', handler);
|
||||||
|
|
||||||
|
expect(returned).toBe(handler);
|
||||||
|
expect(returned()).toBe('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* Parser performance tests
|
||||||
|
* Tests regex pre-compilation and format caching optimizations
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Test the pre-compiled regex patterns directly by importing them from parser.js
|
||||||
|
// We use dynamic import with mocking to avoid the SillyTavern dependency chain
|
||||||
|
|
||||||
|
describe('Parser regex pre-compilation', () => {
|
||||||
|
// Test that pre-compiled regex patterns produce correct results
|
||||||
|
// These patterns are now module-level constants in parser.js
|
||||||
|
|
||||||
|
test('emoji regex pattern works correctly', () => {
|
||||||
|
const EMOJI_RE = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
|
||||||
|
expect('😊 Hello'.match(EMOJI_RE)).toBeTruthy();
|
||||||
|
expect('Hello'.match(EMOJI_RE)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('thinking tag regex works correctly', () => {
|
||||||
|
const THINKING_TAG_RE = /<think>[\s\S]*?<\/think>/gi;
|
||||||
|
const THINKING_TAG_ALT_RE = /<thinking>[\s\S]*?<\/thinking>/gi;
|
||||||
|
|
||||||
|
const text1 = '<think>thinking content</think>actual content';
|
||||||
|
const text2 = '<thinking>thinking content</thinking>actual content';
|
||||||
|
|
||||||
|
expect(text1.replace(THINKING_TAG_RE, '')).toBe('actual content');
|
||||||
|
expect(text2.replace(THINKING_TAG_ALT_RE, '')).toBe('actual content');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('JSON block regex works correctly', () => {
|
||||||
|
const JSON_BLOCK_RE = /```json\s*\n([\s\S]*?)```/g;
|
||||||
|
const text = '```json\n{"key": "value"}\n```';
|
||||||
|
const matches = [...text.matchAll(JSON_BLOCK_RE)];
|
||||||
|
expect(matches.length).toBe(1);
|
||||||
|
expect(matches[0][1].trim()).toBe('{"key": "value"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('code block regex works correctly', () => {
|
||||||
|
const CODE_BLOCK_RE = /```([^`]+)```/g;
|
||||||
|
const text = '```Stats\n---\nHealth: 100%\n```';
|
||||||
|
const matches = [...text.matchAll(CODE_BLOCK_RE)];
|
||||||
|
expect(matches.length).toBe(1);
|
||||||
|
expect(matches[0][1].trim()).toBe('Stats\n---\nHealth: 100%');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('XML trackers regex works correctly', () => {
|
||||||
|
const XML_TRACKERS_RE = /<trackers>([\s\S]*?)<\/trackers>/i;
|
||||||
|
const text = '<trackers>content here</trackers>';
|
||||||
|
const match = text.match(XML_TRACKERS_RE);
|
||||||
|
expect(match).toBeTruthy();
|
||||||
|
expect(match[1]).toBe('content here');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stats section detection regex works', () => {
|
||||||
|
const STATS_SECTION_RE = /Stats\s*\n\s*---/i;
|
||||||
|
expect('Stats\n---\nHealth: 100%'.match(STATS_SECTION_RE)).toBeTruthy();
|
||||||
|
expect('Info Box\n---\nDate: 2024'.match(STATS_SECTION_RE)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('info box section detection regex works', () => {
|
||||||
|
const INFOBOX_SECTION_RE = /Info Box\s*\n\s*---/i;
|
||||||
|
expect('Info Box\n---\nDate: 2024'.match(INFOBOX_SECTION_RE)).toBeTruthy();
|
||||||
|
expect('Stats\n---\nHealth: 100%'.match(INFOBOX_SECTION_RE)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('characters section detection regex works', () => {
|
||||||
|
const CHARACTERS_SECTION_RE = /Present Characters\s*\n\s*---/i;
|
||||||
|
expect('Present Characters\n---\nAlice'.match(CHARACTERS_SECTION_RE)).toBeTruthy();
|
||||||
|
expect('Stats\n---\nHealth: 100%'.match(CHARACTERS_SECTION_RE)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('RPG attribute regexes work', () => {
|
||||||
|
const RPG_STR_RE = /STR:\s*(\d+)/i;
|
||||||
|
const RPG_DEX_RE = /DEX:\s*(\d+)/i;
|
||||||
|
const RPG_LVL_RE = /LVL:\s*(\d+)/i;
|
||||||
|
|
||||||
|
expect('STR: 15'.match(RPG_STR_RE)?.[1]).toBe('15');
|
||||||
|
expect('DEX: 20'.match(RPG_DEX_RE)?.[1]).toBe('20');
|
||||||
|
expect('LVL: 10'.match(RPG_LVL_RE)?.[1]).toBe('10');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('placeholder pattern regex works', () => {
|
||||||
|
const PLACEHOLDER_PATTERN_RE = /\[([A-Za-z\s\/]+)\]/g;
|
||||||
|
const text = '[Location] some text [Mood Emoji]';
|
||||||
|
const matches = [...text.matchAll(PLACEHOLDER_PATTERN_RE)];
|
||||||
|
expect(matches.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('format marker regex works', () => {
|
||||||
|
const FORMAT_MARKER_RE = /FORMAT:\s*/gi;
|
||||||
|
expect('FORMAT: some text'.replace(FORMAT_MARKER_RE, '')).toBe('some text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Format cache logic', () => {
|
||||||
|
test('format cache hit threshold logic', () => {
|
||||||
|
// Simulate the format caching logic from parser.js
|
||||||
|
let lastDetectedFormat = null;
|
||||||
|
let formatCacheHits = 0;
|
||||||
|
const FORMAT_CACHE_HIT_THRESHOLD = 3;
|
||||||
|
|
||||||
|
// Initially no cache
|
||||||
|
expect(lastDetectedFormat).toBeNull();
|
||||||
|
expect(formatCacheHits).toBe(0);
|
||||||
|
|
||||||
|
// First detection
|
||||||
|
lastDetectedFormat = 'json';
|
||||||
|
formatCacheHits = 1;
|
||||||
|
expect(formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD).toBe(false);
|
||||||
|
|
||||||
|
// Second hit
|
||||||
|
formatCacheHits++;
|
||||||
|
expect(formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD).toBe(false);
|
||||||
|
|
||||||
|
// Third hit - now cache is trusted
|
||||||
|
formatCacheHits++;
|
||||||
|
expect(formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD).toBe(true);
|
||||||
|
|
||||||
|
// Cache clear
|
||||||
|
lastDetectedFormat = null;
|
||||||
|
formatCacheHits = 0;
|
||||||
|
expect(lastDetectedFormat).toBeNull();
|
||||||
|
expect(formatCacheHits).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Regex performance verification', () => {
|
||||||
|
test('pre-compiled regex is faster than re-compiling', () => {
|
||||||
|
// Pre-compiled constant (module-level)
|
||||||
|
const PRECOMPILED_RE = /```json\s*\n([\s\S]*?)```/g;
|
||||||
|
|
||||||
|
// Simulate per-call compilation (old behavior)
|
||||||
|
const compilePerCall = (text) => {
|
||||||
|
const re = /```json\s*\n([\s\S]*?)```/g;
|
||||||
|
return [...text.matchAll(re)];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-compiled usage (new behavior)
|
||||||
|
const usePrecompiled = (text) => {
|
||||||
|
// Reset lastIndex for global regex
|
||||||
|
PRECOMPILED_RE.lastIndex = 0;
|
||||||
|
return [...text.matchAll(PRECOMPILED_RE)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const testText = '```json\n{"key": "value"}\n```';
|
||||||
|
|
||||||
|
// Both should produce the same result
|
||||||
|
const compiled = compilePerCall(testText);
|
||||||
|
const precompiled = usePrecompiled(testText);
|
||||||
|
|
||||||
|
expect(compiled.length).toBe(precompiled.length);
|
||||||
|
expect(compiled[0][1]).toBe(precompiled[0][1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import { createStringValidator, sanitizeLocationName, sanitizeItemName, validateStoredInventory, cleanItemString, MAX_ITEMS_PER_SECTION } from '../src/utils/security.js';
|
||||||
|
|
||||||
|
describe('createStringValidator', () => {
|
||||||
|
test('creates a validator that trims whitespace', () => {
|
||||||
|
const validator = createStringValidator({
|
||||||
|
label: 'test',
|
||||||
|
maxLength: 100,
|
||||||
|
checkBlockedNames: false
|
||||||
|
});
|
||||||
|
expect(validator(' hello ')).toBe('hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for non-string input', () => {
|
||||||
|
const validator = createStringValidator({
|
||||||
|
label: 'test',
|
||||||
|
maxLength: 100
|
||||||
|
});
|
||||||
|
expect(validator(null)).toBeNull();
|
||||||
|
expect(validator(undefined)).toBeNull();
|
||||||
|
expect(validator(123)).toBeNull();
|
||||||
|
expect(validator('')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('truncates strings exceeding maxLength', () => {
|
||||||
|
const validator = createStringValidator({
|
||||||
|
label: 'test',
|
||||||
|
maxLength: 5
|
||||||
|
});
|
||||||
|
const result = validator('hello world');
|
||||||
|
expect(result).toBe('hello');
|
||||||
|
expect(result.length).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects blocked property names when checkBlockedNames is true', () => {
|
||||||
|
const validator = createStringValidator({
|
||||||
|
label: 'property',
|
||||||
|
maxLength: 100,
|
||||||
|
checkBlockedNames: true
|
||||||
|
});
|
||||||
|
expect(validator('__proto__')).toBeNull();
|
||||||
|
expect(validator('constructor')).toBeNull();
|
||||||
|
expect(validator('prototype')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows blocked property names when checkBlockedNames is false', () => {
|
||||||
|
const validator = createStringValidator({
|
||||||
|
label: 'item',
|
||||||
|
maxLength: 100,
|
||||||
|
checkBlockedNames: false
|
||||||
|
});
|
||||||
|
// __proto__ is allowed when checkBlockedNames is false
|
||||||
|
expect(validator('__proto__')).toBe('__proto__');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects additional values via additionalRejects', () => {
|
||||||
|
const validator = createStringValidator({
|
||||||
|
label: 'item',
|
||||||
|
maxLength: 100,
|
||||||
|
additionalRejects: ['none', 'null', 'undefined'],
|
||||||
|
checkBlockedNames: false
|
||||||
|
});
|
||||||
|
expect(validator('none')).toBeNull();
|
||||||
|
expect(validator('NONE')).toBeNull();
|
||||||
|
expect(validator('None')).toBeNull();
|
||||||
|
expect(validator('Sword')).toBe('Sword');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is case-insensitive for blocked names', () => {
|
||||||
|
const validator = createStringValidator({
|
||||||
|
label: 'test',
|
||||||
|
maxLength: 100,
|
||||||
|
checkBlockedNames: true
|
||||||
|
});
|
||||||
|
expect(validator('__PROTO__')).toBeNull();
|
||||||
|
expect(validator('__Proto__')).toBeNull();
|
||||||
|
expect(validator('CONSTRUCTOR')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns valid input unchanged', () => {
|
||||||
|
const validator = createStringValidator({
|
||||||
|
label: 'test',
|
||||||
|
maxLength: 100
|
||||||
|
});
|
||||||
|
expect(validator('ValidName')).toBe('ValidName');
|
||||||
|
expect(validator('another-valid-name')).toBe('another-valid-name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sanitizeLocationName', () => {
|
||||||
|
test('allows valid location names', () => {
|
||||||
|
expect(sanitizeLocationName('Home')).toBe('Home');
|
||||||
|
expect(sanitizeLocationName('Tavern')).toBe('Tavern');
|
||||||
|
expect(sanitizeLocationName('Forest Clearing')).toBe('Forest Clearing');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks dangerous property names', () => {
|
||||||
|
expect(sanitizeLocationName('__proto__')).toBeNull();
|
||||||
|
expect(sanitizeLocationName('constructor')).toBeNull();
|
||||||
|
expect(sanitizeLocationName('prototype')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('truncates overly long names', () => {
|
||||||
|
const longName = 'A'.repeat(250);
|
||||||
|
const result = sanitizeLocationName(longName);
|
||||||
|
expect(result.length).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for invalid input', () => {
|
||||||
|
expect(sanitizeLocationName(null)).toBeNull();
|
||||||
|
expect(sanitizeLocationName('')).toBeNull();
|
||||||
|
expect(sanitizeLocationName(' ')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sanitizeItemName', () => {
|
||||||
|
test('allows valid item names', () => {
|
||||||
|
expect(sanitizeItemName('Sword')).toBe('Sword');
|
||||||
|
expect(sanitizeItemName('Health Potion')).toBe('Health Potion');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects "none" as an item name', () => {
|
||||||
|
expect(sanitizeItemName('none')).toBeNull();
|
||||||
|
expect(sanitizeItemName('None')).toBeNull();
|
||||||
|
expect(sanitizeItemName('NONE')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('truncates overly long names', () => {
|
||||||
|
const longName = 'A'.repeat(600);
|
||||||
|
const result = sanitizeItemName(longName);
|
||||||
|
expect(result.length).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for invalid input', () => {
|
||||||
|
expect(sanitizeItemName(null)).toBeNull();
|
||||||
|
expect(sanitizeItemName('')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateStoredInventory', () => {
|
||||||
|
test('returns cleaned inventory object', () => {
|
||||||
|
const input = { Home: 'Sword, Shield' };
|
||||||
|
const result = validateStoredInventory(input);
|
||||||
|
expect(result).toEqual({ Home: 'Sword, Shield' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes dangerous keys', () => {
|
||||||
|
const input = { Home: 'Sword' };
|
||||||
|
// __proto__ as a key is blocked by sanitizeLocationName
|
||||||
|
Object.setPrototypeOf(input, Object.create(null));
|
||||||
|
input['__proto__'] = 'malicious';
|
||||||
|
const result = validateStoredInventory(input);
|
||||||
|
// Result should not have __proto__ as an own property
|
||||||
|
expect(Object.prototype.hasOwnProperty.call(result, '__proto__')).toBe(false);
|
||||||
|
expect(result).toHaveProperty('Home');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty object for invalid input', () => {
|
||||||
|
expect(validateStoredInventory(null)).toEqual({});
|
||||||
|
expect(validateStoredInventory(undefined)).toEqual({});
|
||||||
|
expect(validateStoredInventory('string')).toEqual({});
|
||||||
|
expect(validateStoredInventory([])).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips non-string values', () => {
|
||||||
|
const input = { Home: 123, Barn: 'Hay' };
|
||||||
|
const result = validateStoredInventory(input);
|
||||||
|
expect(result).not.toHaveProperty('Home');
|
||||||
|
expect(result).toHaveProperty('Barn');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cleanItemString', () => {
|
||||||
|
test('returns clean item string', () => {
|
||||||
|
expect(cleanItemString('Sword, Shield')).toBe('Sword, Shield');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('item sanitizer does not block __proto__ (by design - checkBlockedNames is false)', () => {
|
||||||
|
// sanitizeItemName has checkBlockedNames: false, so __proto__ is allowed as item name
|
||||||
|
// Only sanitizeLocationName blocks __proto__ (for object keys)
|
||||||
|
const result = cleanItemString('Sword, Shield');
|
||||||
|
expect(result).toContain('Sword');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('strips markdown formatting', () => {
|
||||||
|
const result = cleanItemString('**Sword**, *Shield*');
|
||||||
|
expect(result).not.toContain('**');
|
||||||
|
expect(result).not.toContain('*');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('MAX_ITEMS_PER_SECTION', () => {
|
||||||
|
test('is a positive number', () => {
|
||||||
|
expect(MAX_ITEMS_PER_SECTION).toBeGreaterThan(0);
|
||||||
|
expect(typeof MAX_ITEMS_PER_SECTION).toBe('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
["@babel/preset-env", {
|
||||||
|
"targets": {
|
||||||
|
"node": "current"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -50,10 +50,12 @@ import {
|
|||||||
setEquipmentContainer,
|
setEquipmentContainer,
|
||||||
setQuestsContainer,
|
setQuestsContainer,
|
||||||
setMusicPlayerContainer,
|
setMusicPlayerContainer,
|
||||||
clearSessionAvatarPrompts
|
clearSessionAvatarPrompts,
|
||||||
|
clearDomCache,
|
||||||
|
clearDebugLogs
|
||||||
} from './src/core/state.js';
|
} from './src/core/state.js';
|
||||||
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData, commitTrackerDataFromPriorMessage } from './src/core/persistence.js';
|
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData, commitTrackerDataFromPriorMessage } from './src/core/persistence.js';
|
||||||
import { registerAllEvents } from './src/core/events.js';
|
import { registerAllEvents, on as onEvent, unregisterAllEvents } from './src/core/events.js';
|
||||||
import { addExtensionSettings } from './src/core/settingsPanel.js';
|
import { addExtensionSettings } from './src/core/settingsPanel.js';
|
||||||
|
|
||||||
// Generation & Parsing modules
|
// Generation & Parsing modules
|
||||||
@@ -106,7 +108,6 @@ import {
|
|||||||
setupDiceRoller,
|
setupDiceRoller,
|
||||||
setupSettingsPopup,
|
setupSettingsPopup,
|
||||||
updateDiceDisplay,
|
updateDiceDisplay,
|
||||||
addDiceQuickReply,
|
|
||||||
getSettingsModal,
|
getSettingsModal,
|
||||||
showWelcomeModalIfNeeded,
|
showWelcomeModalIfNeeded,
|
||||||
showDeprecationModalIfNeeded
|
showDeprecationModalIfNeeded
|
||||||
@@ -185,7 +186,7 @@ import {
|
|||||||
} from './src/systems/integration/sillytavern.js';
|
} from './src/systems/integration/sillytavern.js';
|
||||||
|
|
||||||
// Settings UI event listeners (extracted from initUI)
|
// Settings UI event listeners (extracted from initUI)
|
||||||
import { bindSettingsListeners, updateWeatherSubOptionsVisibility } from './src/systems/ui/settingsListeners.js';
|
import { bindSettingsListeners, initializeSettingsUIState } from './src/systems/ui/settingsListeners.js';
|
||||||
|
|
||||||
// Set up thought-based expressions refresh handler
|
// Set up thought-based expressions refresh handler
|
||||||
setThoughtBasedExpressionsRefreshHandler(() => {
|
setThoughtBasedExpressionsRefreshHandler(() => {
|
||||||
@@ -205,6 +206,71 @@ function updateDynamicLabels() {
|
|||||||
updateMobileTabLabels();
|
updateMobileTabLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers all SillyTavern event handlers for the extension.
|
||||||
|
* Idempotent per enable cycle: call unregisterAllEvents() before re-registering
|
||||||
|
* (e.g. when the extension is re-enabled after being disabled).
|
||||||
|
*/
|
||||||
|
function registerExtensionEvents() {
|
||||||
|
registerAllEvents({
|
||||||
|
[event_types.MESSAGE_SENT]: onMessageSent,
|
||||||
|
[event_types.GENERATION_STARTED]: onGenerationStarted,
|
||||||
|
[event_types.MESSAGE_RECEIVED]: onMessageReceived,
|
||||||
|
[event_types.GENERATION_STOPPED]: onGenerationEnded,
|
||||||
|
[event_types.GENERATION_ENDED]: onGenerationEnded,
|
||||||
|
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts, clearDebugLogs],
|
||||||
|
[event_types.CHAT_LOADED]: onChatLoaded,
|
||||||
|
[event_types.MESSAGE_DELETED]: onMessageDeleted,
|
||||||
|
[event_types.MESSAGE_SWIPE_DELETED]: onMessageDeleted,
|
||||||
|
[event_types.MESSAGE_SWIPED]: onMessageSwiped,
|
||||||
|
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
||||||
|
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use tracked event registration (onEvent) instead of direct eventSource.on()
|
||||||
|
// This ensures all handlers are tracked for cleanup by unregisterAllEvents()
|
||||||
|
onEvent(event_types.CHARACTER_MESSAGE_RENDERED, (messageId) => {
|
||||||
|
if (!extensionSettings.enabled) return;
|
||||||
|
const renderedMessage = chat[messageId];
|
||||||
|
if (renderedMessage && !renderedMessage.is_user && !renderedMessage.is_system) {
|
||||||
|
queueThoughtBasedExpressionsUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onEvent(event_types.MESSAGE_UPDATED, (messageId) => {
|
||||||
|
if (!extensionSettings.enabled) return;
|
||||||
|
const updatedMessage = chat[messageId];
|
||||||
|
if (updatedMessage && !updatedMessage.is_user && !updatedMessage.is_system) {
|
||||||
|
queueThoughtBasedExpressionsUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onEvent(event_types.MESSAGE_SWIPED, (messageIndex) => {
|
||||||
|
if (!extensionSettings.enabled) return;
|
||||||
|
const swipedMessage = chat[messageIndex];
|
||||||
|
if (swipedMessage && !swipedMessage.is_user && !swipedMessage.is_system) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onEvent(event_types.CHAT_CHANGED, () => {
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
onEvent(event_types.MESSAGE_DELETED, () => {
|
||||||
|
if (!extensionSettings.enabled) return;
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
onEvent(event_types.MESSAGE_SWIPE_DELETED, () => {
|
||||||
|
if (!extensionSettings.enabled) return;
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the UI for the extension.
|
* Initializes the UI for the extension.
|
||||||
*/
|
*/
|
||||||
@@ -233,6 +299,8 @@ async function initUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache UI elements using state setters
|
// Cache UI elements using state setters
|
||||||
|
// Clear stale DOM references first to prevent memory leaks on panel rebuild
|
||||||
|
clearDomCache();
|
||||||
setPanelContainer($('#rpg-companion-panel'));
|
setPanelContainer($('#rpg-companion-panel'));
|
||||||
setUserStatsContainer($('#rpg-user-stats'));
|
setUserStatsContainer($('#rpg-user-stats'));
|
||||||
setInfoBoxContainer($('#rpg-info-box'));
|
setInfoBoxContainer($('#rpg-info-box'));
|
||||||
@@ -248,6 +316,19 @@ async function initUI() {
|
|||||||
// Bind all settings event listeners (extracted to settingsListeners.js)
|
// Bind all settings event listeners (extracted to settingsListeners.js)
|
||||||
bindSettingsListeners($);
|
bindSettingsListeners($);
|
||||||
|
|
||||||
|
// Sync settings modal inputs with saved settings, then apply startup UI state
|
||||||
|
initializeSettingsUIState();
|
||||||
|
updatePanelVisibility();
|
||||||
|
updateSectionVisibility();
|
||||||
|
updateGenerationModeUI();
|
||||||
|
applyTheme();
|
||||||
|
applyPanelPosition();
|
||||||
|
toggleCustomColors();
|
||||||
|
toggleAnimations();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
togglePlotButtons();
|
||||||
|
initWeatherEffects();
|
||||||
|
|
||||||
// Initialize mobile UI
|
// Initialize mobile UI
|
||||||
setupMobileToggle();
|
setupMobileToggle();
|
||||||
constrainFabToViewport();
|
constrainFabToViewport();
|
||||||
@@ -301,7 +382,7 @@ jQuery(async () => {
|
|||||||
|
|
||||||
// Load settings with validation
|
// Load settings with validation
|
||||||
try {
|
try {
|
||||||
loadSettings();
|
await loadSettings();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Settings load failed, continuing with defaults:', error);
|
console.error('[RPG Companion] Settings load failed, continuing with defaults:', error);
|
||||||
}
|
}
|
||||||
@@ -325,7 +406,7 @@ jQuery(async () => {
|
|||||||
|
|
||||||
// Add extension settings to Extensions tab
|
// Add extension settings to Extensions tab
|
||||||
try {
|
try {
|
||||||
await addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel);
|
await addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel, registerExtensionEvents, unregisterAllEvents, initHistoryInjection);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Failed to add extension settings tab:', error);
|
console.error('[RPG Companion] Failed to add extension settings tab:', error);
|
||||||
}
|
}
|
||||||
@@ -345,6 +426,9 @@ jQuery(async () => {
|
|||||||
initThoughtBasedExpressions();
|
initThoughtBasedExpressions();
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
updateStripWidgets();
|
updateStripWidgets();
|
||||||
|
// Ensure section visibility matches settings (equipment is hidden by CSS, needs explicit .show())
|
||||||
|
updateSectionVisibility();
|
||||||
|
renderEquipment();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Chat data load failed, using defaults:', error);
|
console.error('[RPG Companion] Chat data load failed, using defaults:', error);
|
||||||
}
|
}
|
||||||
@@ -366,61 +450,7 @@ jQuery(async () => {
|
|||||||
|
|
||||||
// Register all event listeners
|
// Register all event listeners
|
||||||
try {
|
try {
|
||||||
registerAllEvents({
|
registerExtensionEvents();
|
||||||
[event_types.MESSAGE_SENT]: onMessageSent,
|
|
||||||
[event_types.GENERATION_STARTED]: onGenerationStarted,
|
|
||||||
[event_types.MESSAGE_RECEIVED]: onMessageReceived,
|
|
||||||
[event_types.GENERATION_STOPPED]: onGenerationEnded,
|
|
||||||
[event_types.GENERATION_ENDED]: onGenerationEnded,
|
|
||||||
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts],
|
|
||||||
[event_types.CHAT_LOADED]: onChatLoaded,
|
|
||||||
[event_types.MESSAGE_DELETED]: onMessageDeleted,
|
|
||||||
[event_types.MESSAGE_SWIPE_DELETED]: onMessageDeleted,
|
|
||||||
[event_types.MESSAGE_SWIPED]: onMessageSwiped,
|
|
||||||
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
|
||||||
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (messageId) => {
|
|
||||||
if (!extensionSettings.enabled) return;
|
|
||||||
const renderedMessage = chat[messageId];
|
|
||||||
if (renderedMessage && !renderedMessage.is_user && !renderedMessage.is_system) {
|
|
||||||
queueThoughtBasedExpressionsUpdate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.on(event_types.MESSAGE_UPDATED, (messageId) => {
|
|
||||||
if (!extensionSettings.enabled) return;
|
|
||||||
const updatedMessage = chat[messageId];
|
|
||||||
if (updatedMessage && !updatedMessage.is_user && !updatedMessage.is_system) {
|
|
||||||
queueThoughtBasedExpressionsUpdate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.on(event_types.MESSAGE_SWIPED, (messageIndex) => {
|
|
||||||
if (!extensionSettings.enabled) return;
|
|
||||||
const swipedMessage = chat[messageIndex];
|
|
||||||
if (swipedMessage && !swipedMessage.is_user && !swipedMessage.is_system) {
|
|
||||||
queueThoughtBasedExpressionsUpdate({ immediate: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
|
||||||
clearThoughtBasedExpressionsCache();
|
|
||||||
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.on(event_types.MESSAGE_DELETED, () => {
|
|
||||||
if (!extensionSettings.enabled) return;
|
|
||||||
clearThoughtBasedExpressionsCache();
|
|
||||||
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.on(event_types.MESSAGE_SWIPE_DELETED, () => {
|
|
||||||
if (!extensionSettings.enabled) return;
|
|
||||||
clearThoughtBasedExpressionsCache();
|
|
||||||
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Event registration failed:', error);
|
console.error('[RPG Companion] Event registration failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/__tests__/**/*.test.js'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.js$': 'babel-jest',
|
||||||
|
},
|
||||||
|
transformIgnorePatterns: ['/node_modules/'],
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Jest setup file - set up mocks before tests run
|
||||||
|
const mockState = {
|
||||||
|
extensionSettings: {
|
||||||
|
debugMode: false,
|
||||||
|
userStats: {},
|
||||||
|
trackerConfig: {},
|
||||||
|
quests: { main: '', optional: [] }
|
||||||
|
},
|
||||||
|
FEATURE_FLAGS: { useNewInventory: false },
|
||||||
|
addDebugLog: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPersistence = {
|
||||||
|
saveSettings: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockInventoryParser = {
|
||||||
|
extractInventory: () => null
|
||||||
|
};
|
||||||
|
|
||||||
|
// We can't easily mock ESM imports in Jest without transform
|
||||||
|
// Instead, we'll create mock files that Jest can use
|
||||||
Generated
+8045
-1
File diff suppressed because it is too large
Load Diff
+9
-3
@@ -6,6 +6,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:css": "node scripts/build-css.js",
|
"build:css": "node scripts/build-css.js",
|
||||||
|
"build:css:min": "node scripts/build-css.js --minify",
|
||||||
|
"test": "jest",
|
||||||
"validate_locale": "node src/i18n/validator.js --watch",
|
"validate_locale": "node src/i18n/validator.js --watch",
|
||||||
"validate_locale_once": "node src/i18n/validator.js"
|
"validate_locale_once": "node src/i18n/validator.js"
|
||||||
},
|
},
|
||||||
@@ -13,9 +15,13 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^8.0.1",
|
||||||
|
"@babel/preset-env": "^8.0.2",
|
||||||
|
"@jest/globals": "^30.4.1",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
|
"execa": "^9.6.1",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.3",
|
||||||
"glob": "^13.0.6"
|
"glob": "^13.0.6",
|
||||||
},
|
"jest": "^30.4.2"
|
||||||
"dependencies": {}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-9
@@ -3,7 +3,9 @@
|
|||||||
* CSS Build Script
|
* CSS Build Script
|
||||||
* Concatenates modular CSS files from src/styles/ into style.css
|
* Concatenates modular CSS files from src/styles/ into style.css
|
||||||
*
|
*
|
||||||
* Usage: node scripts/build-css.js
|
* Usage:
|
||||||
|
* node scripts/build-css.js # Development build (readable)
|
||||||
|
* node scripts/build-css.js --minify # Production build (minified)
|
||||||
*
|
*
|
||||||
* File order matters! Files are concatenated in alphabetical order.
|
* File order matters! Files are concatenated in alphabetical order.
|
||||||
* Prefix files with numbers to control order: _01_variables.css, _02_panel.css, etc.
|
* Prefix files with numbers to control order: _01_variables.css, _02_panel.css, etc.
|
||||||
@@ -19,8 +21,62 @@ const rootDir = path.resolve(__dirname, '..');
|
|||||||
const stylesDir = path.join(rootDir, 'src', 'styles');
|
const stylesDir = path.join(rootDir, 'src', 'styles');
|
||||||
const outputFile = path.join(rootDir, 'style.css');
|
const outputFile = path.join(rootDir, 'style.css');
|
||||||
|
|
||||||
|
const shouldMinify = process.argv.includes('--minify');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minify CSS by removing comments, unnecessary whitespace, and redundant characters.
|
||||||
|
* Safe for CSS custom properties, strings, and data URIs.
|
||||||
|
*/
|
||||||
|
function minifyCSS(css) {
|
||||||
|
let result = css;
|
||||||
|
|
||||||
|
// Remove CSS comments (/* ... */) — but not inside strings
|
||||||
|
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||||
|
|
||||||
|
// Remove lines that are only whitespace after comment removal
|
||||||
|
result = result.replace(/^\s*[\r\n]/g, '\n');
|
||||||
|
|
||||||
|
// Collapse multiple newlines into one
|
||||||
|
result = result.replace(/\n{2,}/g, '\n');
|
||||||
|
|
||||||
|
// Remove spaces around structural characters outside of strings
|
||||||
|
result = result.replace(/(\{|\}|;|:|,)/g, '$1');
|
||||||
|
|
||||||
|
// Remove space before {
|
||||||
|
result = result.replace(/\s+\{/g, '{');
|
||||||
|
|
||||||
|
// Remove space after {
|
||||||
|
result = result.replace(/\{\s+/g, '{');
|
||||||
|
|
||||||
|
// Remove space before }
|
||||||
|
result = result.replace(/\s+\}/g, '}');
|
||||||
|
|
||||||
|
// Remove space after }
|
||||||
|
result = result.replace(/\}\s+/g, '}');
|
||||||
|
|
||||||
|
// Remove space around :
|
||||||
|
result = result.replace(/\s*:\s*/g, ':');
|
||||||
|
|
||||||
|
// Remove space around ;
|
||||||
|
result = result.replace(/;\s*/g, ';');
|
||||||
|
|
||||||
|
// Remove space around ,
|
||||||
|
result = result.replace(/,\s*/g, ',');
|
||||||
|
|
||||||
|
// Remove trailing semicolon before }
|
||||||
|
result = result.replace(/;\}/g, '}');
|
||||||
|
|
||||||
|
// Collapse multiple spaces into one (preserves spaces inside strings roughly)
|
||||||
|
result = result.replace(/[ \t]+/g, ' ');
|
||||||
|
|
||||||
|
// Remove leading/trailing whitespace
|
||||||
|
result = result.trim();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async function buildCSS() {
|
async function buildCSS() {
|
||||||
console.log('Building CSS from src/styles/...');
|
console.log(`Building CSS from src/styles/...${shouldMinify ? ' (minified)' : ' (readable)'}`);
|
||||||
|
|
||||||
// Read all CSS files from src/styles/
|
// Read all CSS files from src/styles/
|
||||||
const files = await fs.readdir(stylesDir);
|
const files = await fs.readdir(stylesDir);
|
||||||
@@ -42,15 +98,30 @@ async function buildCSS() {
|
|||||||
for (const file of cssFiles) {
|
for (const file of cssFiles) {
|
||||||
const content = await fs.readFile(file, 'utf-8');
|
const content = await fs.readFile(file, 'utf-8');
|
||||||
const basename = path.basename(file);
|
const basename = path.basename(file);
|
||||||
contents.push(`/* ============================================ */`);
|
|
||||||
contents.push(`/* ${basename} */`);
|
if (shouldMinify) {
|
||||||
contents.push(`/* ============================================ */`);
|
// In minified mode, skip comment separators
|
||||||
contents.push('');
|
contents.push(content);
|
||||||
contents.push(content);
|
} else {
|
||||||
contents.push('');
|
contents.push(`/* ============================================ */`);
|
||||||
|
contents.push(`/* ${basename} */`);
|
||||||
|
contents.push(`/* ============================================ */`);
|
||||||
|
contents.push('');
|
||||||
|
contents.push(content);
|
||||||
|
contents.push('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const combined = contents.join('\n');
|
let combined = contents.join('\n');
|
||||||
|
|
||||||
|
// Apply minification if requested
|
||||||
|
if (shouldMinify) {
|
||||||
|
const beforeSize = combined.length;
|
||||||
|
combined = minifyCSS(combined);
|
||||||
|
const afterSize = combined.length;
|
||||||
|
const reduction = (((beforeSize - afterSize) / beforeSize) * 100).toFixed(1);
|
||||||
|
console.log(`\n🗜️ Minification: ${beforeSize.toLocaleString()} → ${afterSize.toLocaleString()} bytes (${reduction}% reduction)`);
|
||||||
|
}
|
||||||
|
|
||||||
// Write to style.css
|
// Write to style.css
|
||||||
await fs.writeFile(outputFile, combined, 'utf-8');
|
await fs.writeFile(outputFile, combined, 'utf-8');
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
|
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
|
||||||
v3.7.2
|
v3.7.4
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+283
-15
@@ -13,17 +13,20 @@ export const extensionName = 'third-party/rpg-companion-sillytavern';
|
|||||||
* This supports both global (public/extensions) and user-specific (data/default-user/extensions) installations
|
* This supports both global (public/extensions) and user-specific (data/default-user/extensions) installations
|
||||||
*/
|
*/
|
||||||
const currentScriptPath = import.meta.url;
|
const currentScriptPath = import.meta.url;
|
||||||
const isUserExtension = currentScriptPath.includes('/data/') || currentScriptPath.includes('\\data\\');
|
const isUserExtension = currentScriptPath.includes('/data/') || currentScriptPath.includes('\\\\data\\\\');
|
||||||
export const extensionFolderPath = isUserExtension
|
export const extensionFolderPath = isUserExtension
|
||||||
? `data/default-user/extensions/${extensionName}`
|
? `data/default-user/extensions/${extensionName}`
|
||||||
: `scripts/extensions/${extensionName}`;
|
: `scripts/extensions/${extensionName}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default extension settings
|
* Default extension settings — single source of truth for all setting defaults.
|
||||||
|
* Imported by state.js to initialize extensionSettings.
|
||||||
*/
|
*/
|
||||||
export const defaultSettings = {
|
export const defaultSettings = {
|
||||||
|
settingsVersion: 7, // Version number for settings migrations
|
||||||
enabled: true,
|
enabled: true,
|
||||||
autoUpdate: true,
|
debugMode: false, // Enable verbose debug logging (developer flag)
|
||||||
|
autoUpdate: false,
|
||||||
updateDepth: 4, // How many messages to include in the context
|
updateDepth: 4, // How many messages to include in the context
|
||||||
generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately
|
generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately
|
||||||
showUserStats: true,
|
showUserStats: true,
|
||||||
@@ -33,37 +36,94 @@ export const defaultSettings = {
|
|||||||
enableThoughtBasedExpressions: false,
|
enableThoughtBasedExpressions: false,
|
||||||
hideDefaultExpressionDisplay: false,
|
hideDefaultExpressionDisplay: false,
|
||||||
showInventory: true, // Show inventory section (v2 system)
|
showInventory: true, // Show inventory section (v2 system)
|
||||||
|
showEquipment: true, // Show equipment section
|
||||||
showQuests: true, // Show quests section
|
showQuests: true, // Show quests section
|
||||||
showLockIcons: true, // Show lock/unlock icons on tracker items
|
showLockIcons: true, // Show lock/unlock icons on tracker items
|
||||||
showThoughtsInChat: true, // Show thoughts overlay in chat
|
showThoughtsInChat: true, // Show thoughts overlay in chat
|
||||||
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
|
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
|
||||||
|
narratorMode: false, // Use character card as narrator instead of fixed character references
|
||||||
|
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
|
||||||
|
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
|
||||||
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
||||||
|
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
|
||||||
|
enableDialogueColoring: false, // Enable dialogue coloring prompt injection
|
||||||
|
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
|
||||||
|
enableDeceptionSystem: false, // Enable deception tracking with <lie> tags
|
||||||
|
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
|
||||||
|
enableOmniscienceFilter: false, // Enable omniscience filter with <ofilter> tags
|
||||||
|
customOmnisciencePrompt: '', // Custom omniscience filter prompt text (empty = use default)
|
||||||
|
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
|
||||||
|
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
|
||||||
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
||||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
||||||
// Controls when the extension skips injecting tracker instructions/examples/HTML
|
|
||||||
// into generations that appear to be user-injected instructions. Valid values:
|
enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default)
|
||||||
// - 'none' -> never skip (legacy behavior: always inject)
|
weatherBackground: true, // Show weather effects in background (behind chat)
|
||||||
// - 'guided' -> skip for any guided / instruct or quiet_prompt generation
|
weatherForeground: false, // Show weather effects in foreground (on top of chat)
|
||||||
// - 'impersonation' -> skip only for impersonation-style guided generations
|
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
|
||||||
// This setting helps compatibility with other extensions like GuidedGenerations.
|
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
|
||||||
skipInjectionsForGuided: 'none',
|
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
|
||||||
enablePlotButtons: true, // Show plot progression buttons above chat input
|
showDeceptionToggle: true, // Show Deception System toggle in main panel
|
||||||
saveTrackerHistory: false, // Save tracker data in chat history for each message
|
showOmniscienceToggle: true, // Show Omniscience Filter toggle in main panel
|
||||||
|
showCYOAToggle: true, // Show CYOA toggle in main panel
|
||||||
|
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
|
||||||
|
|
||||||
|
showDynamicWeatherToggle: true, // Show Dynamic Weather Effects toggle in main panel
|
||||||
|
showNarratorMode: true, // Show Narrator Mode toggle in main panel
|
||||||
|
showAutoAvatars: true, // Show Auto-generate Avatars toggle in main panel
|
||||||
|
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
|
||||||
|
enableRandomizedPlot: true, // Show randomized plot progression button above chat input
|
||||||
|
enableNaturalPlot: true, // Show natural plot progression button above chat input
|
||||||
|
// History persistence settings - inject selected tracker data into historical messages
|
||||||
|
historyPersistence: {
|
||||||
|
enabled: false, // Master toggle for history persistence feature
|
||||||
|
messageCount: 5, // Number of messages to include (0 = all available)
|
||||||
|
injectionPosition: 'assistant_message_end', // 'user_message_end', 'assistant_message_end', 'extra_user_message', 'extra_assistant_message'
|
||||||
|
contextPreamble: '', // Optional custom preamble text (empty = use default short one)
|
||||||
|
sendAllEnabledOnRefresh: false // If true, sends all enabled stats from preset instead of only persistInHistory-enabled stats on Refresh RPG Info
|
||||||
|
},
|
||||||
panelPosition: 'right', // 'left', 'right', or 'top'
|
panelPosition: 'right', // 'left', 'right', or 'top'
|
||||||
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
||||||
customColors: {
|
customColors: {
|
||||||
bg: '#1a1a2e',
|
bg: '#1a1a2e',
|
||||||
|
bgOpacity: 100,
|
||||||
accent: '#16213e',
|
accent: '#16213e',
|
||||||
|
accentOpacity: 100,
|
||||||
text: '#eaeaea',
|
text: '#eaeaea',
|
||||||
highlight: '#e94560'
|
textOpacity: 100,
|
||||||
|
highlight: '#e94560',
|
||||||
|
highlightOpacity: 100
|
||||||
},
|
},
|
||||||
statBarColorLow: '#cc3333', // Color for low stat values (red)
|
statBarColorLow: '#cc3333', // Color for low stat values (red)
|
||||||
|
statBarColorLowOpacity: 100,
|
||||||
statBarColorHigh: '#33cc66', // Color for high stat values (green)
|
statBarColorHigh: '#33cc66', // Color for high stat values (green)
|
||||||
|
statBarColorHighOpacity: 100,
|
||||||
enableAnimations: true, // Enable smooth animations for stats and content updates
|
enableAnimations: true, // Enable smooth animations for stats and content updates
|
||||||
mobileFabPosition: {
|
mobileFabPosition: {
|
||||||
top: 'calc(var(--topBarBlockSize) + 60px)',
|
top: 'calc(var(--topBarBlockSize) + 60px)',
|
||||||
right: '12px'
|
right: '12px'
|
||||||
}, // Saved position for mobile FAB button
|
}, // Saved position for mobile FAB button
|
||||||
|
// Mobile FAB widget display options (8-position system around the button)
|
||||||
|
mobileFabWidgets: {
|
||||||
|
enabled: true, // Master toggle for FAB widgets
|
||||||
|
weatherIcon: { enabled: true, position: 0 }, // Weather emoji (☀️, 🌧️, etc.)
|
||||||
|
weatherDesc: { enabled: true, position: 1 }, // Weather description text
|
||||||
|
clock: { enabled: true, position: 2 }, // Current time display
|
||||||
|
date: { enabled: true, position: 3 }, // Date display
|
||||||
|
location: { enabled: true, position: 4 }, // Location name
|
||||||
|
stats: { enabled: true, position: 5 }, // All stats as compact numbers
|
||||||
|
attributes: { enabled: true, position: 6 } // Compact RPG attributes display
|
||||||
|
},
|
||||||
|
// Desktop strip widget display options (shown in collapsed panel strip)
|
||||||
|
desktopStripWidgets: {
|
||||||
|
enabled: true, // Master toggle for strip widgets (enabled by default)
|
||||||
|
weatherIcon: { enabled: true }, // Weather emoji (☀️, 🌧️, etc.)
|
||||||
|
clock: { enabled: true }, // Current time display
|
||||||
|
date: { enabled: true }, // Date display
|
||||||
|
location: { enabled: true }, // Location name
|
||||||
|
stats: { enabled: true }, // All stats as compact numbers
|
||||||
|
attributes: { enabled: true } // Compact RPG attributes display
|
||||||
|
},
|
||||||
userStats: {
|
userStats: {
|
||||||
health: 100,
|
health: 100,
|
||||||
satiety: 100,
|
satiety: 100,
|
||||||
@@ -72,14 +132,162 @@ export const defaultSettings = {
|
|||||||
arousal: 0,
|
arousal: 0,
|
||||||
mood: '😐',
|
mood: '😐',
|
||||||
conditions: 'None',
|
conditions: 'None',
|
||||||
/** @type {InventoryV2} */
|
skills: [],
|
||||||
inventory: {
|
inventory: {
|
||||||
version: 2,
|
version: 2,
|
||||||
onPerson: "None",
|
onPerson: "None",
|
||||||
|
clothing: "None",
|
||||||
stored: {},
|
stored: {},
|
||||||
assets: "None"
|
assets: "None"
|
||||||
|
},
|
||||||
|
equipment: {
|
||||||
|
items: [], // Array of {id, name, type, slot, stats: {str: 2, dex: 1, ...}, description}
|
||||||
|
slots: {
|
||||||
|
helmet: null,
|
||||||
|
ring1: null,
|
||||||
|
ring2: null,
|
||||||
|
ring3: null,
|
||||||
|
ring4: null,
|
||||||
|
ring5: null,
|
||||||
|
ring6: null,
|
||||||
|
ring7: null,
|
||||||
|
ring8: null,
|
||||||
|
ring9: null,
|
||||||
|
ring10: null,
|
||||||
|
necklace: null,
|
||||||
|
bodyArmor: null,
|
||||||
|
pants: null,
|
||||||
|
shoes: null,
|
||||||
|
gloves: null,
|
||||||
|
accessory1: null,
|
||||||
|
accessory2: null,
|
||||||
|
accessory3: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
statNames: {
|
||||||
|
health: 'Health',
|
||||||
|
satiety: 'Satiety',
|
||||||
|
energy: 'Energy',
|
||||||
|
hygiene: 'Hygiene',
|
||||||
|
arousal: 'Arousal'
|
||||||
|
},
|
||||||
|
// Tracker customization configuration
|
||||||
|
trackerConfig: {
|
||||||
|
userStats: {
|
||||||
|
// Stats display mode: 'percentage' or 'number'
|
||||||
|
statsDisplayMode: 'percentage',
|
||||||
|
// Array of custom stats (allows add/remove/rename)
|
||||||
|
customStats: [
|
||||||
|
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
|
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
|
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
|
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
|
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false, maxValue: 100 }
|
||||||
|
],
|
||||||
|
// RPG Attributes (customizable D&D-style attributes)
|
||||||
|
showRPGAttributes: true,
|
||||||
|
showLevel: true, // Show/hide level in UI and prompts
|
||||||
|
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
|
||||||
|
rpgAttributes: [
|
||||||
|
{ id: 'str', name: 'STR', enabled: true, persistInHistory: false },
|
||||||
|
{ id: 'dex', name: 'DEX', enabled: true, persistInHistory: false },
|
||||||
|
{ id: 'con', name: 'CON', enabled: true, persistInHistory: false },
|
||||||
|
{ id: 'int', name: 'INT', enabled: true, persistInHistory: false },
|
||||||
|
{ id: 'wis', name: 'WIS', enabled: true, persistInHistory: false },
|
||||||
|
{ id: 'cha', name: 'CHA', enabled: true, persistInHistory: false }
|
||||||
|
],
|
||||||
|
// Status section config
|
||||||
|
statusSection: {
|
||||||
|
enabled: true,
|
||||||
|
showMoodEmoji: true,
|
||||||
|
customFields: ['Conditions'], // User can edit what to track
|
||||||
|
persistInHistory: false // Persist status in historical messages
|
||||||
|
},
|
||||||
|
// Optional skills field
|
||||||
|
skillsSection: {
|
||||||
|
enabled: false,
|
||||||
|
label: 'Skills', // User-editable
|
||||||
|
customFields: [], // Array of skill names
|
||||||
|
persistInHistory: false // Persist skills in historical messages
|
||||||
|
},
|
||||||
|
// Inventory persistence
|
||||||
|
inventoryPersistInHistory: false, // Persist inventory in historical messages
|
||||||
|
// Quests persistence
|
||||||
|
questsPersistInHistory: false // Persist quests in historical messages
|
||||||
|
},
|
||||||
|
infoBox: {
|
||||||
|
widgets: {
|
||||||
|
date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, // Date enabled by default for history
|
||||||
|
weather: { enabled: true, persistInHistory: true }, // Weather enabled by default for history
|
||||||
|
temperature: { enabled: true, unit: 'C', persistInHistory: false }, // 'C' or 'F'
|
||||||
|
time: { enabled: true, persistInHistory: true }, // Time enabled by default for history
|
||||||
|
location: { enabled: true, persistInHistory: true }, // Location enabled by default for history
|
||||||
|
recentEvents: { enabled: true, persistInHistory: false }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
presentCharacters: {
|
||||||
|
// Fixed fields (always shown)
|
||||||
|
showEmoji: true,
|
||||||
|
showName: true,
|
||||||
|
// Relationship fields configuration
|
||||||
|
relationships: {
|
||||||
|
enabled: true,
|
||||||
|
// Relationship to emoji mapping (shown on character portraits)
|
||||||
|
relationshipEmojis: {
|
||||||
|
'Lover': '❤️',
|
||||||
|
'Friend': '⭐',
|
||||||
|
'Ally': '🤝',
|
||||||
|
'Enemy': '⚔️',
|
||||||
|
'Neutral': '⚖️'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Legacy fields kept for backward compatibility
|
||||||
|
relationshipFields: ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral'],
|
||||||
|
relationshipEmojis: {
|
||||||
|
'Lover': '❤️',
|
||||||
|
'Friend': '⭐',
|
||||||
|
'Ally': '🤝',
|
||||||
|
'Enemy': '⚔️',
|
||||||
|
'Neutral': '⚖️'
|
||||||
|
},
|
||||||
|
// Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |)
|
||||||
|
customFields: [
|
||||||
|
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)', persistInHistory: false },
|
||||||
|
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false }
|
||||||
|
],
|
||||||
|
// Thoughts configuration (separate line)
|
||||||
|
thoughts: {
|
||||||
|
enabled: true,
|
||||||
|
name: 'Thoughts',
|
||||||
|
description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)',
|
||||||
|
persistInHistory: false
|
||||||
|
},
|
||||||
|
// Character stats toggle (optional feature)
|
||||||
|
characterStats: {
|
||||||
|
enabled: false,
|
||||||
|
customStats: [
|
||||||
|
{ id: 'health', name: 'Health', enabled: true },
|
||||||
|
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quests: {
|
||||||
|
main: "None", // Current main quest title
|
||||||
|
optional: [] // Array of optional quest titles
|
||||||
|
},
|
||||||
|
infoBox: JSON.stringify({
|
||||||
|
date: { value: new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) },
|
||||||
|
weather: { emoji: '☀️', forecast: 'Clear skies' },
|
||||||
|
temperature: { value: 20, unit: 'C' },
|
||||||
|
time: { start: '00:00', end: '00:00' },
|
||||||
|
location: { value: 'Unknown Location' }
|
||||||
|
}, null, 2),
|
||||||
|
characterThoughts: JSON.stringify({
|
||||||
|
characters: []
|
||||||
|
}, null, 2),
|
||||||
|
level: 1, // User's character level
|
||||||
classicStats: {
|
classicStats: {
|
||||||
str: 10,
|
str: 10,
|
||||||
dex: 10,
|
dex: 10,
|
||||||
@@ -89,5 +297,65 @@ export const defaultSettings = {
|
|||||||
cha: 10
|
cha: 10
|
||||||
},
|
},
|
||||||
lastDiceRoll: null, // Store last dice roll result
|
lastDiceRoll: null, // Store last dice roll result
|
||||||
collapsedInventoryLocations: [] // Array of collapsed storage location names
|
showDiceDisplay: true, // Show the "Last Roll" display in the panel
|
||||||
|
collapsedInventoryLocations: [], // Array of collapsed storage location names
|
||||||
|
inventoryViewModes: {
|
||||||
|
onPerson: 'list', // 'list' or 'grid' view mode for On Person section
|
||||||
|
stored: 'list', // 'list' or 'grid' view mode for Stored section
|
||||||
|
assets: 'list' // 'list' or 'grid' view mode for Assets section
|
||||||
|
},
|
||||||
|
npcAvatars: {}, // Store custom avatar images for NPCs (key: character name, value: base64 data URI)
|
||||||
|
// Combat encounter settings
|
||||||
|
encounterSettings: {
|
||||||
|
enabled: true, // Show Start Encounter button above chat input
|
||||||
|
historyDepth: 8, // Number of recent messages to include in combat initialization
|
||||||
|
autoSaveLogs: false // Save detailed combat logs to file
|
||||||
|
},
|
||||||
|
// Auto avatar generation settings
|
||||||
|
autoGenerateAvatars: true, // Master toggle for auto-generating avatars
|
||||||
|
avatarLLMCustomInstruction: '', // Custom instruction for LLM prompt generation
|
||||||
|
// External API settings for 'external' generation mode
|
||||||
|
externalApiSettings: {
|
||||||
|
baseUrl: '', // OpenAI-compatible API base URL (e.g., "https://api.openai.com/v1")
|
||||||
|
// apiKey is NOT stored here for security. It is stored in localStorage('rpg_companion_external_api_key')
|
||||||
|
model: '', // Model identifier (e.g., "gpt-4o-mini")
|
||||||
|
maxTokens: 8192, // Maximum tokens for generation
|
||||||
|
temperature: 0.7 // Temperature setting for generation
|
||||||
|
},
|
||||||
|
// Lock state for tracker items (v3 JSON format feature)
|
||||||
|
lockedItems: {
|
||||||
|
stats: [], // Array of locked stat IDs (e.g., ["health", "satiety"])
|
||||||
|
skills: [], // Array of locked skill names (e.g., ["Cooking", "Swordsmanship"])
|
||||||
|
inventory: {
|
||||||
|
onPerson: [], // Array of locked item indices (e.g., [0, 2])
|
||||||
|
clothing: [], // Array of locked item indices
|
||||||
|
stored: {}, // Object with location keys, each containing array of locked indices (e.g., {"Home": [0, 1]})
|
||||||
|
assets: [] // Array of locked asset indices
|
||||||
|
},
|
||||||
|
quests: {
|
||||||
|
main: false, // Boolean for main quest lock
|
||||||
|
optional: [] // Array of locked optional quest indices (e.g., [0, 2])
|
||||||
|
},
|
||||||
|
infoBox: {
|
||||||
|
date: false, // Boolean for date widget lock
|
||||||
|
weather: false, // Boolean for weather widget lock
|
||||||
|
temperature: false, // Boolean for temperature widget lock
|
||||||
|
time: false, // Boolean for time widget lock
|
||||||
|
location: false, // Boolean for location widget lock
|
||||||
|
recentEvents: false // Boolean for recent events widget lock
|
||||||
|
},
|
||||||
|
characters: {} // Object mapping character names to their locked fields (e.g., {"Sarah": {relationship: true, thoughts: false}})
|
||||||
|
},
|
||||||
|
// Preset management for tracker configurations
|
||||||
|
presetManager: {
|
||||||
|
// Map of preset ID to preset data (contains name and trackerConfig)
|
||||||
|
presets: {},
|
||||||
|
// Map of character/group entity to preset ID (e.g., "char_0": "preset_123", "group_abc": "preset_456")
|
||||||
|
// Note: This is stored separately and NOT exported with presets
|
||||||
|
characterAssociations: {},
|
||||||
|
// Currently active preset ID
|
||||||
|
activePresetId: null,
|
||||||
|
// Default preset ID (used when no character association exists)
|
||||||
|
defaultPresetId: null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ import { eventSource, event_types } from '../../../../../../script.js';
|
|||||||
*/
|
*/
|
||||||
export function on(eventType, handler) {
|
export function on(eventType, handler) {
|
||||||
eventSource.on(eventType, handler);
|
eventSource.on(eventType, handler);
|
||||||
|
|
||||||
|
// Track for cleanup
|
||||||
|
if (!registeredHandlers.has(eventType)) {
|
||||||
|
registeredHandlers.set(eventType, []);
|
||||||
|
}
|
||||||
|
registeredHandlers.get(eventType).push(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,6 +27,12 @@ export function on(eventType, handler) {
|
|||||||
*/
|
*/
|
||||||
export function once(eventType, handler) {
|
export function once(eventType, handler) {
|
||||||
eventSource.once(eventType, handler);
|
eventSource.once(eventType, handler);
|
||||||
|
|
||||||
|
// Track for cleanup (one-time handlers should also be tracked)
|
||||||
|
if (!registeredHandlers.has(eventType)) {
|
||||||
|
registeredHandlers.set(eventType, []);
|
||||||
|
}
|
||||||
|
registeredHandlers.get(eventType).push(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +42,18 @@ export function once(eventType, handler) {
|
|||||||
*/
|
*/
|
||||||
export function off(eventType, handler) {
|
export function off(eventType, handler) {
|
||||||
eventSource.off(eventType, handler);
|
eventSource.off(eventType, handler);
|
||||||
|
|
||||||
|
// Remove from tracking so unregisterAllEvents() doesn't hold a stale reference
|
||||||
|
const handlers = registeredHandlers.get(eventType);
|
||||||
|
if (handlers) {
|
||||||
|
const index = handlers.indexOf(handler);
|
||||||
|
if (index !== -1) {
|
||||||
|
handlers.splice(index, 1);
|
||||||
|
}
|
||||||
|
if (handlers.length === 0) {
|
||||||
|
registeredHandlers.delete(eventType);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -559,7 +559,7 @@ function validateSettings(settings) {
|
|||||||
* Loads the extension settings from the global settings object.
|
* Loads the extension settings from the global settings object.
|
||||||
* Automatically migrates v1 inventory to v2 format if needed.
|
* Automatically migrates v1 inventory to v2 format if needed.
|
||||||
*/
|
*/
|
||||||
export function loadSettings() {
|
export async function loadSettings() {
|
||||||
try {
|
try {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const extension_settings = context.extension_settings || context.extensionSettings;
|
const extension_settings = context.extension_settings || context.extensionSettings;
|
||||||
@@ -601,7 +601,7 @@ export function loadSettings() {
|
|||||||
// Migration to version 3: Convert text trackers to JSON format
|
// Migration to version 3: Convert text trackers to JSON format
|
||||||
if (currentVersion < 3) {
|
if (currentVersion < 3) {
|
||||||
// console.log('[RPG Companion] Migrating settings to version 3 (JSON tracker format)');
|
// console.log('[RPG Companion] Migrating settings to version 3 (JSON tracker format)');
|
||||||
migrateToV3JSON();
|
await migrateToV3JSON();
|
||||||
extensionSettings.settingsVersion = 3;
|
extensionSettings.settingsVersion = 3;
|
||||||
settingsChanged = true;
|
settingsChanged = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ import { i18n } from './i18n.js';
|
|||||||
* @param {Function} injectCheckpointButton - Add checkpoint buttons
|
* @param {Function} injectCheckpointButton - Add checkpoint buttons
|
||||||
* @param {Function} updateAllCheckpointIndicators - Update checkpoint button states
|
* @param {Function} updateAllCheckpointIndicators - Update checkpoint button states
|
||||||
* @param {Function} removeAlternatePresentCharactersPanel - Remove alt characters panel
|
* @param {Function} removeAlternatePresentCharactersPanel - Remove alt characters panel
|
||||||
|
* @param {Function} registerExtensionEvents - (Re)register all SillyTavern event handlers
|
||||||
|
* @param {Function} unregisterAllEvents - Unregister all tracked SillyTavern event handlers
|
||||||
|
* @param {Function} initHistoryInjection - (Re)register history injection listeners
|
||||||
*/
|
*/
|
||||||
export async function addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel) {
|
export async function addExtensionSettings($, renderExtensionTemplateAsync, clearExtensionPrompts, updateChatThoughts, cleanupCheckpointUI, clearThoughtBasedExpressionsCache, toggleDynamicWeather, initUI, loadChatData, scheduleChatStateRehydration, initThoughtBasedExpressions, injectCheckpointButton, updateAllCheckpointIndicators, removeAlternatePresentCharactersPanel, registerExtensionEvents, unregisterAllEvents, initHistoryInjection) {
|
||||||
const settingsHtml = await renderExtensionTemplateAsync(extensionName, 'settings');
|
const settingsHtml = await renderExtensionTemplateAsync(extensionName, 'settings');
|
||||||
$('#extensions_settings2').append(settingsHtml);
|
$('#extensions_settings2').append(settingsHtml);
|
||||||
|
|
||||||
@@ -43,12 +46,18 @@ export async function addExtensionSettings($, renderExtensionTemplateAsync, clea
|
|||||||
clearThoughtBasedExpressionsCache();
|
clearThoughtBasedExpressionsCache();
|
||||||
toggleDynamicWeather(false);
|
toggleDynamicWeather(false);
|
||||||
|
|
||||||
|
// Unregister all tracked event handlers to avoid leaks while disabled
|
||||||
|
unregisterAllEvents();
|
||||||
|
|
||||||
$('#rpg-companion-panel').remove();
|
$('#rpg-companion-panel').remove();
|
||||||
$('#rpg-mobile-toggle').remove();
|
$('#rpg-mobile-toggle').remove();
|
||||||
$('#rpg-collapse-toggle').remove();
|
$('#rpg-collapse-toggle').remove();
|
||||||
$('#rpg-plot-buttons').remove();
|
$('#rpg-plot-buttons').remove();
|
||||||
removeAlternatePresentCharactersPanel();
|
removeAlternatePresentCharactersPanel();
|
||||||
} else if (extensionSettings.enabled && !wasEnabled) {
|
} else if (extensionSettings.enabled && !wasEnabled) {
|
||||||
|
// Re-register event handlers that were unregistered on disable
|
||||||
|
registerExtensionEvents();
|
||||||
|
initHistoryInjection();
|
||||||
await initUI();
|
await initUI();
|
||||||
loadChatData();
|
loadChatData();
|
||||||
scheduleChatStateRehydration();
|
scheduleChatStateRehydration();
|
||||||
|
|||||||
+29
-335
@@ -3,347 +3,16 @@
|
|||||||
* Centralizes all extension state variables
|
* Centralizes all extension state variables
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { defaultSettings } from './config.js';
|
||||||
|
|
||||||
// Type imports
|
// Type imports
|
||||||
/** @typedef {import('../types/inventory.js').InventoryV2} InventoryV2 */
|
/** @typedef {import('../types/inventory.js').InventoryV2} InventoryV2 */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension settings - persisted to SillyTavern settings
|
* Extension settings - persisted to SillyTavern settings
|
||||||
|
* Initialized from config.js defaultSettings (single source of truth)
|
||||||
*/
|
*/
|
||||||
export let extensionSettings = {
|
export let extensionSettings = { ...defaultSettings };
|
||||||
settingsVersion: 6, // Version number for settings migrations
|
|
||||||
enabled: true,
|
|
||||||
autoUpdate: false,
|
|
||||||
updateDepth: 4, // How many messages to include in the context
|
|
||||||
generationMode: 'together', // 'separate' or 'together' - whether to generate with main response or separately
|
|
||||||
showUserStats: true,
|
|
||||||
showInfoBox: true,
|
|
||||||
showCharacterThoughts: true,
|
|
||||||
showAlternatePresentCharactersPanel: false,
|
|
||||||
enableThoughtBasedExpressions: false,
|
|
||||||
hideDefaultExpressionDisplay: false,
|
|
||||||
showInventory: true, // Show inventory section (v2 system)
|
|
||||||
showEquipment: true, // Show equipment section
|
|
||||||
showQuests: true, // Show quests section
|
|
||||||
showThoughtsInChat: true, // Show thoughts overlay in chat
|
|
||||||
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
|
|
||||||
narratorMode: false, // Use character card as narrator instead of fixed character references
|
|
||||||
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
|
|
||||||
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
|
|
||||||
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
|
||||||
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
|
|
||||||
enableDialogueColoring: false, // Enable dialogue coloring prompt injection
|
|
||||||
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
|
|
||||||
enableDeceptionSystem: false, // Enable deception tracking with <lie> tags
|
|
||||||
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
|
|
||||||
enableOmniscienceFilter: false, // Enable omniscience filter with <ofilter> tags
|
|
||||||
customOmnisciencePrompt: '', // Custom omniscience filter prompt text (empty = use default)
|
|
||||||
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
|
|
||||||
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
|
|
||||||
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
|
||||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
|
||||||
|
|
||||||
enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default)
|
|
||||||
weatherBackground: true, // Show weather effects in background (behind chat)
|
|
||||||
weatherForeground: false, // Show weather effects in foreground (on top of chat)
|
|
||||||
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
|
|
||||||
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
|
|
||||||
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
|
|
||||||
showDeceptionToggle: true, // Show Deception System toggle in main panel
|
|
||||||
showOmniscienceToggle: true, // Show Omniscience Filter toggle in main panel
|
|
||||||
showCYOAToggle: true, // Show CYOA toggle in main panel
|
|
||||||
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
|
|
||||||
|
|
||||||
showDynamicWeatherToggle: true, // Show Dynamic Weather Effects toggle in main panel
|
|
||||||
showNarratorMode: true, // Show Narrator Mode toggle in main panel
|
|
||||||
showAutoAvatars: true, // Show Auto-generate Avatars toggle in main panel
|
|
||||||
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
|
|
||||||
enableRandomizedPlot: true, // Show randomized plot progression button above chat input
|
|
||||||
enableNaturalPlot: true, // Show natural plot progression button above chat input
|
|
||||||
// History persistence settings - inject selected tracker data into historical messages
|
|
||||||
historyPersistence: {
|
|
||||||
enabled: false, // Master toggle for history persistence feature
|
|
||||||
messageCount: 5, // Number of messages to include (0 = all available)
|
|
||||||
injectionPosition: 'assistant_message_end', // 'user_message_end', 'assistant_message_end', 'extra_user_message', 'extra_assistant_message'
|
|
||||||
contextPreamble: '', // Optional custom preamble text (empty = use default short one)
|
|
||||||
sendAllEnabledOnRefresh: false // If true, sends all enabled stats from preset instead of only persistInHistory-enabled stats on Refresh RPG Info
|
|
||||||
},
|
|
||||||
panelPosition: 'right', // 'left', 'right', or 'top'
|
|
||||||
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
|
||||||
customColors: {
|
|
||||||
bg: '#1a1a2e',
|
|
||||||
bgOpacity: 100,
|
|
||||||
accent: '#16213e',
|
|
||||||
accentOpacity: 100,
|
|
||||||
text: '#eaeaea',
|
|
||||||
textOpacity: 100,
|
|
||||||
highlight: '#e94560',
|
|
||||||
highlightOpacity: 100
|
|
||||||
},
|
|
||||||
statBarColorLow: '#cc3333', // Color for low stat values (red)
|
|
||||||
statBarColorLowOpacity: 100,
|
|
||||||
statBarColorHigh: '#33cc66', // Color for high stat values (green)
|
|
||||||
statBarColorHighOpacity: 100,
|
|
||||||
enableAnimations: true, // Enable smooth animations for stats and content updates
|
|
||||||
mobileFabPosition: {
|
|
||||||
top: 'calc(var(--topBarBlockSize) + 60px)',
|
|
||||||
right: '12px'
|
|
||||||
}, // Saved position for mobile FAB button
|
|
||||||
// Mobile FAB widget display options (8-position system around the button)
|
|
||||||
mobileFabWidgets: {
|
|
||||||
enabled: true, // Master toggle for FAB widgets
|
|
||||||
weatherIcon: { enabled: true, position: 0 }, // Weather emoji (☀️, 🌧️, etc.)
|
|
||||||
weatherDesc: { enabled: true, position: 1 }, // Weather description text
|
|
||||||
clock: { enabled: true, position: 2 }, // Current time display
|
|
||||||
date: { enabled: true, position: 3 }, // Date display
|
|
||||||
location: { enabled: true, position: 4 }, // Location name
|
|
||||||
stats: { enabled: true, position: 5 }, // All stats as compact numbers
|
|
||||||
attributes: { enabled: true, position: 6 } // Compact RPG attributes display
|
|
||||||
},
|
|
||||||
// Desktop strip widget display options (shown in collapsed panel strip)
|
|
||||||
desktopStripWidgets: {
|
|
||||||
enabled: true, // Master toggle for strip widgets (enabled by default)
|
|
||||||
weatherIcon: { enabled: true }, // Weather emoji (☀️, 🌧️, etc.)
|
|
||||||
clock: { enabled: true }, // Current time display
|
|
||||||
date: { enabled: true }, // Date display
|
|
||||||
location: { enabled: true }, // Location name
|
|
||||||
stats: { enabled: true }, // All stats as compact numbers
|
|
||||||
attributes: { enabled: true } // Compact RPG attributes display
|
|
||||||
},
|
|
||||||
userStats: {
|
|
||||||
health: 100,
|
|
||||||
satiety: 100,
|
|
||||||
energy: 100,
|
|
||||||
hygiene: 100,
|
|
||||||
arousal: 0,
|
|
||||||
mood: '😐',
|
|
||||||
conditions: 'None',
|
|
||||||
skills: [],
|
|
||||||
inventory: {
|
|
||||||
version: 2,
|
|
||||||
onPerson: "None",
|
|
||||||
clothing: "None",
|
|
||||||
stored: {},
|
|
||||||
assets: "None"
|
|
||||||
},
|
|
||||||
equipment: {
|
|
||||||
items: [], // Array of {id, name, type, slot, stats: {str: 2, dex: 1, ...}, description}
|
|
||||||
slots: {
|
|
||||||
helmet: null,
|
|
||||||
ring1: null,
|
|
||||||
ring2: null,
|
|
||||||
ring3: null,
|
|
||||||
ring4: null,
|
|
||||||
ring5: null,
|
|
||||||
ring6: null,
|
|
||||||
ring7: null,
|
|
||||||
ring8: null,
|
|
||||||
ring9: null,
|
|
||||||
ring10: null,
|
|
||||||
necklace: null,
|
|
||||||
bodyArmor: null,
|
|
||||||
pants: null,
|
|
||||||
shoes: null,
|
|
||||||
gloves: null,
|
|
||||||
accessory1: null,
|
|
||||||
accessory2: null,
|
|
||||||
accessory3: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
statNames: {
|
|
||||||
health: 'Health',
|
|
||||||
satiety: 'Satiety',
|
|
||||||
energy: 'Energy',
|
|
||||||
hygiene: 'Hygiene',
|
|
||||||
arousal: 'Arousal'
|
|
||||||
},
|
|
||||||
// Tracker customization configuration
|
|
||||||
trackerConfig: {
|
|
||||||
userStats: {
|
|
||||||
// Stats display mode: 'percentage' or 'number'
|
|
||||||
statsDisplayMode: 'percentage',
|
|
||||||
// Array of custom stats (allows add/remove/rename)
|
|
||||||
customStats: [
|
|
||||||
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false, maxValue: 100 },
|
|
||||||
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false, maxValue: 100 },
|
|
||||||
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false, maxValue: 100 },
|
|
||||||
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false, maxValue: 100 },
|
|
||||||
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false, maxValue: 100 }
|
|
||||||
],
|
|
||||||
// RPG Attributes (customizable D&D-style attributes)
|
|
||||||
showRPGAttributes: true,
|
|
||||||
showLevel: true, // Show/hide level in UI and prompts
|
|
||||||
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
|
|
||||||
rpgAttributes: [
|
|
||||||
{ id: 'str', name: 'STR', enabled: true, persistInHistory: false },
|
|
||||||
{ id: 'dex', name: 'DEX', enabled: true, persistInHistory: false },
|
|
||||||
{ id: 'con', name: 'CON', enabled: true, persistInHistory: false },
|
|
||||||
{ id: 'int', name: 'INT', enabled: true, persistInHistory: false },
|
|
||||||
{ id: 'wis', name: 'WIS', enabled: true, persistInHistory: false },
|
|
||||||
{ id: 'cha', name: 'CHA', enabled: true, persistInHistory: false }
|
|
||||||
],
|
|
||||||
// Status section config
|
|
||||||
statusSection: {
|
|
||||||
enabled: true,
|
|
||||||
showMoodEmoji: true,
|
|
||||||
customFields: ['Conditions'], // User can edit what to track
|
|
||||||
persistInHistory: false // Persist status in historical messages
|
|
||||||
},
|
|
||||||
// Optional skills field
|
|
||||||
skillsSection: {
|
|
||||||
enabled: false,
|
|
||||||
label: 'Skills', // User-editable
|
|
||||||
customFields: [], // Array of skill names
|
|
||||||
persistInHistory: false // Persist skills in historical messages
|
|
||||||
},
|
|
||||||
// Inventory persistence
|
|
||||||
inventoryPersistInHistory: false, // Persist inventory in historical messages
|
|
||||||
// Quests persistence
|
|
||||||
questsPersistInHistory: false // Persist quests in historical messages
|
|
||||||
},
|
|
||||||
infoBox: {
|
|
||||||
widgets: {
|
|
||||||
date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, // Date enabled by default for history
|
|
||||||
weather: { enabled: true, persistInHistory: true }, // Weather enabled by default for history
|
|
||||||
temperature: { enabled: true, unit: 'C', persistInHistory: false }, // 'C' or 'F'
|
|
||||||
time: { enabled: true, persistInHistory: true }, // Time enabled by default for history
|
|
||||||
location: { enabled: true, persistInHistory: true }, // Location enabled by default for history
|
|
||||||
recentEvents: { enabled: true, persistInHistory: false }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
presentCharacters: {
|
|
||||||
// Fixed fields (always shown)
|
|
||||||
showEmoji: true,
|
|
||||||
showName: true,
|
|
||||||
// Relationship fields configuration
|
|
||||||
relationships: {
|
|
||||||
enabled: true,
|
|
||||||
// Relationship to emoji mapping (shown on character portraits)
|
|
||||||
relationshipEmojis: {
|
|
||||||
'Lover': '❤️',
|
|
||||||
'Friend': '⭐',
|
|
||||||
'Ally': '🤝',
|
|
||||||
'Enemy': '⚔️',
|
|
||||||
'Neutral': '⚖️'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Legacy fields kept for backward compatibility
|
|
||||||
relationshipFields: ['Lover', 'Friend', 'Ally', 'Enemy', 'Neutral'],
|
|
||||||
relationshipEmojis: {
|
|
||||||
'Lover': '❤️',
|
|
||||||
'Friend': '⭐',
|
|
||||||
'Ally': '🤝',
|
|
||||||
'Enemy': '⚔️',
|
|
||||||
'Neutral': '⚖️'
|
|
||||||
},
|
|
||||||
// Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |)
|
|
||||||
customFields: [
|
|
||||||
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)', persistInHistory: false },
|
|
||||||
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false }
|
|
||||||
],
|
|
||||||
// Thoughts configuration (separate line)
|
|
||||||
thoughts: {
|
|
||||||
enabled: true,
|
|
||||||
name: 'Thoughts',
|
|
||||||
description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)',
|
|
||||||
persistInHistory: false
|
|
||||||
},
|
|
||||||
// Character stats toggle (optional feature)
|
|
||||||
characterStats: {
|
|
||||||
enabled: false,
|
|
||||||
customStats: [
|
|
||||||
{ id: 'health', name: 'Health', enabled: true },
|
|
||||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
quests: {
|
|
||||||
main: "None", // Current main quest title
|
|
||||||
optional: [] // Array of optional quest titles
|
|
||||||
},
|
|
||||||
infoBox: JSON.stringify({
|
|
||||||
date: { value: new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) },
|
|
||||||
weather: { emoji: '☀️', forecast: 'Clear skies' },
|
|
||||||
temperature: { value: 20, unit: 'C' },
|
|
||||||
time: { start: '00:00', end: '00:00' },
|
|
||||||
location: { value: 'Unknown Location' }
|
|
||||||
}, null, 2),
|
|
||||||
characterThoughts: JSON.stringify({
|
|
||||||
characters: []
|
|
||||||
}, null, 2),
|
|
||||||
level: 1, // User's character level
|
|
||||||
classicStats: {
|
|
||||||
str: 10,
|
|
||||||
dex: 10,
|
|
||||||
con: 10,
|
|
||||||
int: 10,
|
|
||||||
wis: 10,
|
|
||||||
cha: 10
|
|
||||||
},
|
|
||||||
lastDiceRoll: null, // Store last dice roll result
|
|
||||||
showDiceDisplay: true, // Show the "Last Roll" display in the panel
|
|
||||||
collapsedInventoryLocations: [], // Array of collapsed storage location names
|
|
||||||
inventoryViewModes: {
|
|
||||||
onPerson: 'list', // 'list' or 'grid' view mode for On Person section
|
|
||||||
stored: 'list', // 'list' or 'grid' view mode for Stored section
|
|
||||||
assets: 'list' // 'list' or 'grid' view mode for Assets section
|
|
||||||
},
|
|
||||||
npcAvatars: {}, // Store custom avatar images for NPCs (key: character name, value: base64 data URI)
|
|
||||||
// Combat encounter settings
|
|
||||||
encounterSettings: {
|
|
||||||
enabled: true, // Show Start Encounter button above chat input
|
|
||||||
historyDepth: 8, // Number of recent messages to include in combat initialization
|
|
||||||
autoSaveLogs: false // Save detailed combat logs to file
|
|
||||||
},
|
|
||||||
// Auto avatar generation settings
|
|
||||||
autoGenerateAvatars: true, // Master toggle for auto-generating avatars
|
|
||||||
avatarLLMCustomInstruction: '', // Custom instruction for LLM prompt generation
|
|
||||||
// External API settings for 'external' generation mode
|
|
||||||
externalApiSettings: {
|
|
||||||
baseUrl: '', // OpenAI-compatible API base URL (e.g., "https://api.openai.com/v1")
|
|
||||||
// apiKey is NOT stored here for security. It is stored in localStorage('rpg_companion_api_key')
|
|
||||||
model: '', // Model identifier (e.g., "gpt-4o-mini")
|
|
||||||
maxTokens: 8192, // Maximum tokens for generation
|
|
||||||
temperature: 0.7 // Temperature setting for generation
|
|
||||||
},
|
|
||||||
// Lock state for tracker items (v3 JSON format feature)
|
|
||||||
lockedItems: {
|
|
||||||
stats: [], // Array of locked stat IDs (e.g., ["health", "satiety"])
|
|
||||||
skills: [], // Array of locked skill names (e.g., ["Cooking", "Swordsmanship"])
|
|
||||||
inventory: {
|
|
||||||
onPerson: [], // Array of locked item indices (e.g., [0, 2])
|
|
||||||
clothing: [], // Array of locked item indices
|
|
||||||
stored: {}, // Object with location keys, each containing array of locked indices (e.g., {"Home": [0, 1]})
|
|
||||||
assets: [] // Array of locked asset indices
|
|
||||||
},
|
|
||||||
quests: {
|
|
||||||
main: false, // Boolean for main quest lock
|
|
||||||
optional: [] // Array of locked optional quest indices (e.g., [0, 2])
|
|
||||||
},
|
|
||||||
infoBox: {
|
|
||||||
date: false, // Boolean for date widget lock
|
|
||||||
weather: false, // Boolean for weather widget lock
|
|
||||||
temperature: false, // Boolean for temperature widget lock
|
|
||||||
time: false, // Boolean for time widget lock
|
|
||||||
location: false, // Boolean for location widget lock
|
|
||||||
recentEvents: false // Boolean for recent events widget lock
|
|
||||||
},
|
|
||||||
characters: {} // Object mapping character names to their locked fields (e.g., {"Sarah": {relationship: true, thoughts: false}})
|
|
||||||
},
|
|
||||||
// Preset management for tracker configurations
|
|
||||||
presetManager: {
|
|
||||||
// Map of preset ID to preset data (contains name and trackerConfig)
|
|
||||||
presets: {},
|
|
||||||
// Map of character/group entity to preset ID (e.g., "char_0": "preset_123", "group_abc": "preset_456")
|
|
||||||
// Note: This is stored separately and NOT exported with presets
|
|
||||||
characterAssociations: {},
|
|
||||||
// Currently active preset ID
|
|
||||||
activePresetId: null,
|
|
||||||
// Default preset ID (used when no character association exists)
|
|
||||||
defaultPresetId: null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Last generated data from AI response
|
* Last generated data from AI response
|
||||||
@@ -594,3 +263,28 @@ export function setMusicPlayerContainer($element) {
|
|||||||
export function setEquipmentContainer($element) {
|
export function setEquipmentContainer($element) {
|
||||||
$equipmentContainer = $element;
|
$equipmentContainer = $element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all cached DOM element references.
|
||||||
|
* Call this when the panel is destroyed or rebuilt to prevent stale jQuery references.
|
||||||
|
*/
|
||||||
|
export function clearDomCache() {
|
||||||
|
$panelContainer = null;
|
||||||
|
$userStatsContainer = null;
|
||||||
|
$infoBoxContainer = null;
|
||||||
|
$thoughtsContainer = null;
|
||||||
|
$inventoryContainer = null;
|
||||||
|
$questsContainer = null;
|
||||||
|
$musicPlayerContainer = null;
|
||||||
|
$equipmentContainer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the debug logs array.
|
||||||
|
* Call this on chat change to prevent memory accumulation across sessions.
|
||||||
|
*/
|
||||||
|
export function clearDebugLogs() {
|
||||||
|
debugLogs.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+11
-8
@@ -1,7 +1,10 @@
|
|||||||
const fs = require('fs-extra');
|
import fs from 'fs-extra';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const chokidar = require('chokidar');
|
import { fileURLToPath } from 'url';
|
||||||
const glob = require('glob');
|
import chokidar from 'chokidar';
|
||||||
|
import { globSync } from 'glob';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const COMPILED_DIR = __dirname // path.join(__dirname, 'compiled');
|
const COMPILED_DIR = __dirname // path.join(__dirname, 'compiled');
|
||||||
|
|
||||||
@@ -11,7 +14,7 @@ function findUnlocalizedText() {
|
|||||||
|
|
||||||
console.log(`\n🔎 Scanning for unlocalized text in ${srcDir}...`);
|
console.log(`\n🔎 Scanning for unlocalized text in ${srcDir}...`);
|
||||||
|
|
||||||
const files = glob.sync(`${srcDir}/**/*.{html,js,jsx}`, {
|
const files = globSync(`${srcDir}/**/*.{html,js,jsx}`, {
|
||||||
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,7 +104,7 @@ function validateTranslations() {
|
|||||||
|
|
||||||
// Get all keys from reference locale
|
// Get all keys from reference locale
|
||||||
const referenceKeys = Object.keys(translations[referenceLocale]);
|
const referenceKeys = Object.keys(translations[referenceLocale]);
|
||||||
console.log(`🔢 Reference locale has ${referenceKeys.size} unique keys`);
|
console.log(`🔢 Reference locale has ${referenceKeys.length} unique keys`);
|
||||||
|
|
||||||
// Track statistics
|
// Track statistics
|
||||||
const stats = {
|
const stats = {
|
||||||
@@ -127,7 +130,7 @@ function validateTranslations() {
|
|||||||
|
|
||||||
// Check for missing keys
|
// Check for missing keys
|
||||||
for (const key of referenceKeys) {
|
for (const key of referenceKeys) {
|
||||||
if (!key in translations[locale]) {
|
if (!(key in translations[locale])) {
|
||||||
stats.missingKeys[locale].push(key);
|
stats.missingKeys[locale].push(key);
|
||||||
} else {
|
} else {
|
||||||
// Check for type mismatches
|
// Check for type mismatches
|
||||||
@@ -146,7 +149,7 @@ function validateTranslations() {
|
|||||||
|
|
||||||
// Check for extra keys
|
// Check for extra keys
|
||||||
for (const key of localeKeys) {
|
for (const key of localeKeys) {
|
||||||
if (!key in translations[referenceLocale]) {
|
if (!(key in translations[referenceLocale])) {
|
||||||
stats.extraKeys[locale].push(key);
|
stats.extraKeys[locale].push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+114
-217
@@ -10,6 +10,12 @@
|
|||||||
--rpg-highlight: #ff006e;
|
--rpg-highlight: #ff006e;
|
||||||
--rpg-border: #8b00ff;
|
--rpg-border: #8b00ff;
|
||||||
--rpg-shadow: rgba(139, 0, 255, 0.5);
|
--rpg-shadow: rgba(139, 0, 255, 0.5);
|
||||||
|
--rpg-content-bg: linear-gradient(135deg, #1a1f3a 0%, #0a0e27 100%);
|
||||||
|
--rpg-content-box-shadow: 0 0 40px rgba(139, 0, 255, 0.4), inset 0 0 30px rgba(0, 0, 0, 0.5);
|
||||||
|
--rpg-content-border: 2px solid #8b00ff;
|
||||||
|
--rpg-header-text-shadow: 0 0 20px #ff006e, 0 0 40px #8b00ff;
|
||||||
|
--rpg-divider-bg: linear-gradient(to right, transparent, #8b00ff, #ff006e, #8b00ff, transparent);
|
||||||
|
--rpg-thoughts-bg: linear-gradient(135deg, rgba(10, 14, 39, 0.9) 0%, rgba(26, 31, 58, 0.8) 50%, rgba(10, 14, 39, 0.9) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply sci-fi theme to thought panel */
|
/* Apply sci-fi theme to thought panel */
|
||||||
@@ -25,32 +31,6 @@
|
|||||||
--rpg-shadow: rgba(139, 0, 255, 0.5);
|
--rpg-shadow: rgba(139, 0, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-content-box {
|
|
||||||
background: linear-gradient(135deg, #1a1f3a 0%, #0a0e27 100%);
|
|
||||||
box-shadow: 0 0 40px rgba(139, 0, 255, 0.4), inset 0 0 30px rgba(0, 0, 0, 0.5);
|
|
||||||
border: 2px solid #8b00ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-panel-header h3,
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-stats-title,
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-info-header,
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-thoughts-header {
|
|
||||||
text-shadow: 0 0 20px #ff006e, 0 0 40px #8b00ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-divider {
|
|
||||||
background: linear-gradient(to right, transparent, #8b00ff, #ff006e, #8b00ff, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-thoughts-content::before {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
rgba(10, 14, 39, 0.9) 0%,
|
|
||||||
rgba(26, 31, 58, 0.8) 50%,
|
|
||||||
rgba(10, 14, 39, 0.9) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fantasy / Rustic Parchment Theme */
|
/* Fantasy / Rustic Parchment Theme */
|
||||||
.rpg-panel[data-theme="fantasy"] {
|
.rpg-panel[data-theme="fantasy"] {
|
||||||
--rpg-bg: #2b1810;
|
--rpg-bg: #2b1810;
|
||||||
@@ -59,6 +39,16 @@
|
|||||||
--rpg-highlight: #d4af37;
|
--rpg-highlight: #d4af37;
|
||||||
--rpg-border: #8b6914;
|
--rpg-border: #8b6914;
|
||||||
--rpg-shadow: rgba(0, 0, 0, 0.7);
|
--rpg-shadow: rgba(0, 0, 0, 0.7);
|
||||||
|
--rpg-bg-image: linear-gradient(rgba(43, 24, 16, 0.9), rgba(43, 24, 16, 0.9)), url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" /></filter><rect width="100" height="100" filter="url(%23noise)" opacity="0.1"/></svg>');
|
||||||
|
--rpg-content-bg: linear-gradient(135deg, #3d2414 0%, #2b1810 100%);
|
||||||
|
--rpg-content-box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(139, 105, 20, 0.2);
|
||||||
|
--rpg-content-border: 3px solid #8b6914;
|
||||||
|
--rpg-content-border-style: ridge;
|
||||||
|
--rpg-header-font-family: 'Georgia', serif;
|
||||||
|
--rpg-header-text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
--rpg-divider-content: '❦';
|
||||||
|
--rpg-divider-font-size: clamp(1rem, 1.5vw, 1.5rem);
|
||||||
|
--rpg-thoughts-bg: linear-gradient(135deg, rgba(43, 24, 16, 0.92) 0%, rgba(61, 36, 20, 0.88) 50%, rgba(43, 24, 16, 0.92) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply fantasy theme to thought panel */
|
/* Apply fantasy theme to thought panel */
|
||||||
@@ -74,41 +64,6 @@
|
|||||||
--rpg-shadow: rgba(0, 0, 0, 0.7);
|
--rpg-shadow: rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] {
|
|
||||||
background-image:
|
|
||||||
linear-gradient(rgba(43, 24, 16, 0.9), rgba(43, 24, 16, 0.9)),
|
|
||||||
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" /></filter><rect width="100" height="100" filter="url(%23noise)" opacity="0.1"/></svg>');
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-content-box {
|
|
||||||
background: linear-gradient(135deg, #3d2414 0%, #2b1810 100%);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(139, 105, 20, 0.2);
|
|
||||||
border: 3px solid #8b6914;
|
|
||||||
border-style: ridge;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-panel-header h3,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-stats-title,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-info-header,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-thoughts-header {
|
|
||||||
font-family: 'Georgia', serif;
|
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-divider::after {
|
|
||||||
content: '❦';
|
|
||||||
font-size: clamp(1rem, 1.5vw, 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-thoughts-content::before {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
rgba(43, 24, 16, 0.92) 0%,
|
|
||||||
rgba(61, 36, 20, 0.88) 50%,
|
|
||||||
rgba(43, 24, 16, 0.92) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cyberpunk / Neon Grid Theme */
|
/* Cyberpunk / Neon Grid Theme */
|
||||||
.rpg-panel[data-theme="cyberpunk"] {
|
.rpg-panel[data-theme="cyberpunk"] {
|
||||||
--rpg-bg: #000000;
|
--rpg-bg: #000000;
|
||||||
@@ -117,6 +72,17 @@
|
|||||||
--rpg-highlight: #ff2a6d;
|
--rpg-highlight: #ff2a6d;
|
||||||
--rpg-border: #05d9e8;
|
--rpg-border: #05d9e8;
|
||||||
--rpg-shadow: rgba(5, 217, 232, 0.5);
|
--rpg-shadow: rgba(5, 217, 232, 0.5);
|
||||||
|
--rpg-bg-image: linear-gradient(rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.95)), repeating-linear-gradient(0deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px), repeating-linear-gradient(90deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px);
|
||||||
|
--rpg-bg-size: 100% 100%, 30px 30px, 30px 30px;
|
||||||
|
--rpg-content-bg: linear-gradient(135deg, rgba(13, 13, 13, 0.9) 0%, rgba(0, 0, 0, 0.9) 100%);
|
||||||
|
--rpg-content-box-shadow: 0 0 40px rgba(255, 42, 109, 0.4), inset 0 0 30px rgba(5, 217, 232, 0.2);
|
||||||
|
--rpg-content-border: 2px solid #05d9e8;
|
||||||
|
--rpg-header-font-family: 'Courier New', monospace;
|
||||||
|
--rpg-header-letter-spacing: 0.125em;
|
||||||
|
--rpg-header-text-shadow: 0 0 10px #ff2a6d, 0 0 20px #05d9e8, 0 0 30px #ff2a6d;
|
||||||
|
--rpg-divider-bg: linear-gradient(to right, transparent, #05d9e8, #ff2a6d, #05d9e8, transparent);
|
||||||
|
--rpg-divider-height: 0.188rem;
|
||||||
|
--rpg-thoughts-bg: linear-gradient(135deg, rgba(0, 0, 0, 0.92) 0%, rgba(13, 13, 13, 0.85) 50%, rgba(0, 0, 0, 0.92) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply cyberpunk theme to thought panel */
|
/* Apply cyberpunk theme to thought panel */
|
||||||
@@ -132,248 +98,179 @@
|
|||||||
--rpg-shadow: rgba(5, 217, 232, 0.5);
|
--rpg-shadow: rgba(5, 217, 232, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] {
|
|
||||||
background: linear-gradient(rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.95)),
|
|
||||||
repeating-linear-gradient(0deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px),
|
|
||||||
repeating-linear-gradient(90deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px);
|
|
||||||
background-size: 100% 100%, 30px 30px, 30px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-content-box {
|
|
||||||
background: linear-gradient(135deg, rgba(13, 13, 13, 0.9) 0%, rgba(0, 0, 0, 0.9) 100%);
|
|
||||||
box-shadow: 0 0 40px rgba(255, 42, 109, 0.4), inset 0 0 30px rgba(5, 217, 232, 0.2);
|
|
||||||
border: 2px solid #05d9e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-panel-header h3,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-stats-title,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-info-header,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-thoughts-header {
|
|
||||||
text-shadow: 0 0 10px #ff2a6d, 0 0 20px #05d9e8, 0 0 30px #ff2a6d;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
letter-spacing: 0.125em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-divider {
|
|
||||||
background: linear-gradient(to right, transparent, #05d9e8, #ff2a6d, #05d9e8, transparent);
|
|
||||||
height: 0.188rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-thoughts-content::before {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
rgba(0, 0, 0, 0.92) 0%,
|
|
||||||
rgba(13, 13, 13, 0.85) 50%,
|
|
||||||
rgba(0, 0, 0, 0.92) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME SUPPORT FOR ALL PANEL ELEMENTS
|
THEME SUPPORT FOR ALL PANEL ELEMENTS
|
||||||
|
Uses CSS custom properties defined above — no selector duplication needed.
|
||||||
|
Any element inside .rpg-panel inherits the theme variables automatically.
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
/* Apply theme colors to tabs navigation */
|
/* Apply theme background image if defined */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-tabs-nav,
|
.rpg-panel[data-theme] {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-tabs-nav,
|
background-image: var(--rpg-bg-image, none);
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-tabs-nav {
|
background-size: var(--rpg-bg-size, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content box theming via custom properties */
|
||||||
|
.rpg-panel[data-theme] .rpg-content-box {
|
||||||
|
background: var(--rpg-content-bg);
|
||||||
|
box-shadow: var(--rpg-content-box-shadow);
|
||||||
|
border: var(--rpg-content-border);
|
||||||
|
border-style: var(--rpg-content-border-style, solid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header theming */
|
||||||
|
.rpg-panel[data-theme] .rpg-panel-header h3,
|
||||||
|
.rpg-panel[data-theme] .rpg-stats-title,
|
||||||
|
.rpg-panel[data-theme] .rpg-info-header,
|
||||||
|
.rpg-panel[data-theme] .rpg-thoughts-header {
|
||||||
|
font-family: var(--rpg-header-font-family, inherit);
|
||||||
|
text-shadow: var(--rpg-header-text-shadow, none);
|
||||||
|
letter-spacing: var(--rpg-header-letter-spacing, normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Divider theming */
|
||||||
|
.rpg-panel[data-theme] .rpg-divider {
|
||||||
|
background: var(--rpg-divider-bg);
|
||||||
|
height: var(--rpg-divider-height, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-panel[data-theme] .rpg-divider::after {
|
||||||
|
content: var(--rpg-divider-content, none);
|
||||||
|
font-size: var(--rpg-divider-font-size, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thoughts content theming */
|
||||||
|
.rpg-panel[data-theme] .rpg-thoughts-content::before {
|
||||||
|
background: var(--rpg-thoughts-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs navigation theming */
|
||||||
|
.rpg-panel[data-theme] .rpg-tabs-nav {
|
||||||
border-bottom-color: var(--rpg-border);
|
border-bottom-color: var(--rpg-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-tab-btn,
|
.rpg-panel[data-theme] .rpg-tab-btn {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-tab-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-tab-btn {
|
|
||||||
background: var(--rpg-accent);
|
background: var(--rpg-accent);
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-tab-btn:hover,
|
.rpg-panel[data-theme] .rpg-tab-btn:hover {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-tab-btn:hover,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-tab-btn:hover {
|
|
||||||
background: var(--rpg-highlight);
|
background: var(--rpg-highlight);
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-tab-btn.active,
|
.rpg-panel[data-theme] .rpg-tab-btn.active {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-tab-btn.active,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-tab-btn.active {
|
|
||||||
background: var(--rpg-highlight);
|
background: var(--rpg-highlight);
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-bg);
|
color: var(--rpg-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to inventory subtabs */
|
/* Inventory subtabs theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtabs,
|
.rpg-panel[data-theme] .rpg-inventory-subtabs {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtabs,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtabs {
|
|
||||||
border-bottom-color: var(--rpg-border);
|
border-bottom-color: var(--rpg-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtab,
|
.rpg-panel[data-theme] .rpg-inventory-subtab {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtab,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtab {
|
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtab:hover,
|
.rpg-panel[data-theme] .rpg-inventory-subtab:hover {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtab:hover,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtab:hover {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtab.active,
|
.rpg-panel[data-theme] .rpg-inventory-subtab.active {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtab.active,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtab.active {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to quests subtabs */
|
/* Quests subtabs theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtabs,
|
.rpg-panel[data-theme] .rpg-quests-subtabs {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtabs,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtabs {
|
|
||||||
border-bottom-color: var(--rpg-border);
|
border-bottom-color: var(--rpg-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtab,
|
.rpg-panel[data-theme] .rpg-quests-subtab {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtab,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtab {
|
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtab:hover,
|
.rpg-panel[data-theme] .rpg-quests-subtab:hover {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtab:hover,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtab:hover {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtab.active,
|
.rpg-panel[data-theme] .rpg-quests-subtab.active {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtab.active,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtab.active {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to storage locations */
|
/* Storage locations theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-storage-location,
|
.rpg-panel[data-theme] .rpg-storage-location {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-storage-location,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-storage-location {
|
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-storage-header,
|
.rpg-panel[data-theme] .rpg-storage-header {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-storage-header,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-storage-header {
|
|
||||||
background: var(--rpg-highlight);
|
background: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-storage-content,
|
.rpg-panel[data-theme] .rpg-storage-content {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-storage-content,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-storage-content {
|
|
||||||
background: var(--rpg-accent);
|
background: var(--rpg-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to buttons */
|
/* Button theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-edit-btn,
|
.rpg-panel[data-theme] .rpg-inventory-edit-btn,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-add-btn,
|
.rpg-panel[data-theme] .rpg-inventory-add-btn,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-remove-btn,
|
.rpg-panel[data-theme] .rpg-inventory-remove-btn,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit,
|
.rpg-panel[data-theme] .rpg-quest-edit,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-remove,
|
.rpg-panel[data-theme] .rpg-quest-remove,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-add-quest-btn,
|
.rpg-panel[data-theme] .rpg-add-quest-btn {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-edit-btn,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-add-btn,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-remove-btn,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-remove,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-add-quest-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-edit-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-add-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-remove-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-remove,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-add-quest-btn {
|
|
||||||
background: var(--rpg-accent);
|
background: var(--rpg-accent);
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-add-btn,
|
.rpg-panel[data-theme] .rpg-inventory-add-btn,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-add-quest-btn,
|
.rpg-panel[data-theme] .rpg-add-quest-btn {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-add-btn,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-add-quest-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-add-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-add-quest-btn {
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to inventory/quest items */
|
/* Inventory/quest items theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-text,
|
.rpg-panel[data-theme] .rpg-inventory-text,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-item,
|
.rpg-panel[data-theme] .rpg-quest-item,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-main-quest-display,
|
.rpg-panel[data-theme] .rpg-main-quest-display {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-text,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-item,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-main-quest-display,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-text,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-item,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-main-quest-display {
|
|
||||||
background: var(--rpg-accent);
|
background: var(--rpg-accent);
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-item:hover,
|
.rpg-panel[data-theme] .rpg-quest-item:hover {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-item:hover,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-item:hover {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to hints and empty states */
|
/* Hints and empty states theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-hint,
|
.rpg-panel[data-theme] .rpg-inventory-hint,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-hint,
|
.rpg-panel[data-theme] .rpg-quest-hint,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-empty,
|
.rpg-panel[data-theme] .rpg-inventory-empty,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-empty,
|
.rpg-panel[data-theme] .rpg-quest-empty {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-hint,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-hint,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-empty,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-empty,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-hint,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-hint,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-empty,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-empty {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to input fields */
|
/* Input fields theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inline-input,
|
.rpg-panel[data-theme] .rpg-inline-input,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form input,
|
.rpg-panel[data-theme] .rpg-quest-edit-form input,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form textarea,
|
.rpg-panel[data-theme] .rpg-quest-edit-form textarea {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inline-input,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form input,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form textarea,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inline-input,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form input,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form textarea {
|
|
||||||
background: var(--rpg-bg);
|
background: var(--rpg-bg);
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inline-input:focus,
|
.rpg-panel[data-theme] .rpg-inline-input:focus,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form input:focus,
|
.rpg-panel[data-theme] .rpg-quest-edit-form input:focus,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form textarea:focus,
|
.rpg-panel[data-theme] .rpg-quest-edit-form textarea:focus {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inline-input:focus,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form input:focus,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form textarea:focus,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inline-input:focus,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form input:focus,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form textarea:focus {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ export const SLOTS_LIST = Object.entries(EQUIPMENT_CATEGORIES).flatMap(([type, d
|
|||||||
*/
|
*/
|
||||||
export function escapeHtml(text) {
|
export function escapeHtml(text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
const div = document.createElement('div');
|
return String(text)
|
||||||
div.textContent = text;
|
.replace(/&/g, '&')
|
||||||
return div.innerHTML;
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,13 +115,4 @@ export function clearDiceRoll() {
|
|||||||
updateDiceDisplay();
|
updateDiceDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the Roll Dice quick reply button.
|
|
||||||
*/
|
|
||||||
export function addDiceQuickReply() {
|
|
||||||
// Create quick reply button if Quick Replies exist
|
|
||||||
if (window.quickReplyApi) {
|
|
||||||
// Quick Reply API integration would go here
|
|
||||||
// For now, the dice display in the sidebar serves as the button
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { currentEncounter } from '../features/encounterState.js';
|
|||||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
import { isPresentCharactersEnabled } from '../../utils/presentCharacters.js';
|
import { isPresentCharactersEnabled } from '../../utils/presentCharacters.js';
|
||||||
import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js';
|
import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js';
|
||||||
import { applyLocks } from './lockManager.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets character information from the current chat
|
* Gets character information from the current chat
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, eventSource, event_types } from '../../../../../../../script.js';
|
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, event_types } from '../../../../../../../script.js';
|
||||||
|
import { on as onEvent } from '../../core/events.js';
|
||||||
import {
|
import {
|
||||||
extensionSettings,
|
extensionSettings,
|
||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
@@ -38,10 +39,6 @@ let currentSuppressionState = false;
|
|||||||
// Type imports
|
// Type imports
|
||||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||||
|
|
||||||
// Track the latest user message we committed for to prevent duplicate commits
|
|
||||||
// when GENERATION_STARTED can fire multiple times for the same turn.
|
|
||||||
let lastCommittedUserMessageSignature = null;
|
|
||||||
|
|
||||||
// Store context map for prompt injection (used by event handlers)
|
// Store context map for prompt injection (used by event handlers)
|
||||||
let pendingContextMap = new Map();
|
let pendingContextMap = new Map();
|
||||||
|
|
||||||
@@ -892,15 +889,16 @@ ${contextInstructionsText}
|
|||||||
export function initHistoryInjectionListeners() {
|
export function initHistoryInjectionListeners() {
|
||||||
// Register persistent listeners for prompt injection
|
// Register persistent listeners for prompt injection
|
||||||
// These check pendingContextMap and only inject if there's data
|
// These check pendingContextMap and only inject if there's data
|
||||||
|
// Use tracked registration (onEvent) so unregisterAllEvents() cleans them up
|
||||||
|
|
||||||
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
|
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
|
||||||
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
|
onEvent(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
|
||||||
|
|
||||||
// Fallback: AFTER_COMBINE for text completion (string-based injection)
|
// Fallback: AFTER_COMBINE for text completion (string-based injection)
|
||||||
eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
|
onEvent(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
|
||||||
|
|
||||||
// Chat completion (OpenAI, etc.)
|
// Chat completion (OpenAI, etc.)
|
||||||
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
|
onEvent(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
|
||||||
|
|
||||||
console.log('[RPG Companion] History injection listeners initialized');
|
console.log('[RPG Companion] History injection listeners initialized');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
* Helper functions for building JSON format tracker prompts
|
* Helper functions for building JSON format tracker prompts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
import { extensionSettings } from '../../core/state.js';
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
|
||||||
import { getWeatherKeywordsAsPromptString } from '../ui/weatherEffects.js';
|
import { getWeatherKeywordsAsPromptString } from '../ui/weatherEffects.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
@@ -39,7 +38,6 @@ function toFieldKey(name) {
|
|||||||
* @returns {string} JSON format instruction for user stats
|
* @returns {string} JSON format instruction for user stats
|
||||||
*/
|
*/
|
||||||
export function buildUserStatsJSONInstruction() {
|
export function buildUserStatsJSONInstruction() {
|
||||||
const userName = getContext().name1;
|
|
||||||
const trackerConfig = extensionSettings.trackerConfig;
|
const trackerConfig = extensionSettings.trackerConfig;
|
||||||
const userStatsConfig = trackerConfig?.userStats;
|
const userStatsConfig = trackerConfig?.userStats;
|
||||||
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||||
@@ -184,7 +182,6 @@ export function buildInfoBoxJSONInstruction() {
|
|||||||
* @returns {string} JSON format instruction for present characters
|
* @returns {string} JSON format instruction for present characters
|
||||||
*/
|
*/
|
||||||
export function buildCharactersJSONInstruction() {
|
export function buildCharactersJSONInstruction() {
|
||||||
const userName = getContext().name1;
|
|
||||||
const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters;
|
const presentCharsConfig = extensionSettings.trackerConfig?.presentCharacters;
|
||||||
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
|
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
|
||||||
const relationshipsEnabled = presentCharsConfig?.relationships?.enabled !== false;
|
const relationshipsEnabled = presentCharsConfig?.relationships?.enabled !== false;
|
||||||
|
|||||||
@@ -9,6 +9,100 @@ import { saveSettings } from '../../core/persistence.js';
|
|||||||
import { extractInventory } from './inventoryParser.js';
|
import { extractInventory } from './inventoryParser.js';
|
||||||
import { repairJSON, extractJSONFromText } from '../../utils/jsonRepair.js';
|
import { repairJSON, extractJSONFromText } from '../../utils/jsonRepair.js';
|
||||||
|
|
||||||
|
/* ===== Pre-compiled regex patterns (module-level constants) ===== */
|
||||||
|
|
||||||
|
// Thinking tag removal
|
||||||
|
const THINKING_TAG_RE = /<think>[\s\S]*?<\/think>/gi;
|
||||||
|
const THINKING_TAG_ALT_RE = /<thinking>[\s\S]*?<\/thinking>/gi;
|
||||||
|
|
||||||
|
// FORMAT: marker removal
|
||||||
|
const FORMAT_MARKER_RE = /FORMAT:\s*/gi;
|
||||||
|
|
||||||
|
// Emoji extraction (separateEmojiFromText)
|
||||||
|
const EMOJI_RE = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
|
||||||
|
|
||||||
|
// Bracket stripping
|
||||||
|
const PLACEHOLDER_PATTERN_RE = /\[([A-Za-z\s\/]+)\]/g;
|
||||||
|
|
||||||
|
// JSON code block extraction
|
||||||
|
const JSON_BLOCK_RE = /```json\s*\n([\s\S]*?)```/g;
|
||||||
|
|
||||||
|
// XML trackers
|
||||||
|
const XML_TRACKERS_RE = /<trackers>([\s\S]*?)<\/trackers>/i;
|
||||||
|
|
||||||
|
// Code block extraction
|
||||||
|
const CODE_BLOCK_RE = /```([^`]+)```/g;
|
||||||
|
|
||||||
|
// XML text fallback patterns
|
||||||
|
const XML_STATS_MATCH_RE = /(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i;
|
||||||
|
const XML_INFOBOX_MATCH_RE = /Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i;
|
||||||
|
const XML_CHARACTERS_MATCH_RE = /Present Characters\s*\n\s*---[\s\S]*$/i;
|
||||||
|
|
||||||
|
// Combined code block section patterns
|
||||||
|
const COMBINED_STATS_RE = /(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i;
|
||||||
|
const COMBINED_INFOBOX_RE = /Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i;
|
||||||
|
const COMBINED_CHARACTERS_RE = /Present Characters\s*\n\s*---[\s\S]*$/i;
|
||||||
|
|
||||||
|
// Section detection patterns
|
||||||
|
const IS_STATS_HEADER_RE = /Stats\s*\n\s*---/i;
|
||||||
|
const IS_USER_STATS_HEADER_RE = /User Stats\s*\n\s*---/i;
|
||||||
|
const IS_PLAYER_STATS_HEADER_RE = /Player Stats\s*\n\s*---/i;
|
||||||
|
const IS_STATS_HEALTH_RE = /Health:\s*\d+%/i;
|
||||||
|
const IS_STATS_ENERGY_RE = /Energy:\s*\d+%/i;
|
||||||
|
const IS_INFOBOX_HEADER_RE = /Info Box\s*\n\s*---/i;
|
||||||
|
const IS_SCENE_INFO_HEADER_RE = /Scene Info\s*\n\s*---/i;
|
||||||
|
const IS_INFORMATION_HEADER_RE = /Information\s*\n\s*---/i;
|
||||||
|
const IS_INFOBOX_DATE_RE = /Date:/i;
|
||||||
|
const IS_INFOBOX_LOCATION_RE = /Location:/i;
|
||||||
|
const IS_INFOBOX_TIME_RE = /Time:/i;
|
||||||
|
const IS_CHARACTERS_HEADER_RE = /Present Characters\s*\n\s*---/i;
|
||||||
|
const IS_CHARACTERS_ALT_RE = /Characters\s*\n\s*---/i;
|
||||||
|
const IS_CHARACTERS_THOUGHTS_RE = /Character Thoughts\s*\n\s*---/i;
|
||||||
|
const IS_CHARACTERS_BULLET_RE = /^-\s+\w+/m;
|
||||||
|
const IS_CHARACTERS_DETAILS_RE = /Details:/i;
|
||||||
|
|
||||||
|
// Debug pattern checks
|
||||||
|
const DEBUG_STATS_RE = /Stats\s*\n\s*---/i;
|
||||||
|
const DEBUG_INFOBOX_RE = /Info Box\s*\n\s*---/i;
|
||||||
|
const DEBUG_CHARACTERS_RE = /Present Characters\s*\n\s*---/i;
|
||||||
|
|
||||||
|
// Final fallback fenced regex
|
||||||
|
const FENCED_FALLBACK_RE = /```(?:json)?\s*\n?([\s\S]*?)```/gi;
|
||||||
|
|
||||||
|
// parseUserStats patterns
|
||||||
|
const RPG_STR_RE = /STR:\s*(\d+)/i;
|
||||||
|
const RPG_DEX_RE = /DEX:\s*(\d+)/i;
|
||||||
|
const RPG_CON_RE = /CON:\s*(\d+)/i;
|
||||||
|
const RPG_INT_RE = /INT:\s*(\d+)/i;
|
||||||
|
const RPG_WIS_RE = /WIS:\s*(\d+)/i;
|
||||||
|
const RPG_CHA_RE = /CHA:\s*(\d+)/i;
|
||||||
|
const RPG_LVL_RE = /LVL:\s*(\d+)/i;
|
||||||
|
const STATUS_MATCH_RE = /Status:\s*(.+)/i;
|
||||||
|
const SKILLS_MATCH_RE = /Skills:\s*(.+)/i;
|
||||||
|
const INVENTORY_MATCH_RE = /Inventory:\s*(.+)/i;
|
||||||
|
const MAIN_QUEST_MATCH_RE = /Main Quests?:\s*(.+)/i;
|
||||||
|
const OPTIONAL_QUESTS_MATCH_RE = /Optional Quests:\s*(.+)/i;
|
||||||
|
|
||||||
|
// Section detection helpers
|
||||||
|
const STATS_SECTION_RE = /Stats\s*\n\s*---/i;
|
||||||
|
const INFOBOX_SECTION_RE = /Info Box\s*\n\s*---/i;
|
||||||
|
const CHARACTERS_SECTION_RE = /Present Characters\s*\n\s*---/i;
|
||||||
|
|
||||||
|
/* ===== Format detection cache ===== */
|
||||||
|
// Cache the last detected format to skip unnecessary checks on subsequent calls.
|
||||||
|
// The cache is invalidated when the page reloads (module re-initializes).
|
||||||
|
let lastDetectedFormat = null; // 'json', 'json_block', 'xml', 'text', null
|
||||||
|
const FORMAT_CACHE_HIT_THRESHOLD = 3; // Minimum consecutive hits before trusting cache
|
||||||
|
let formatCacheHits = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the format detection cache. Call this when the AI model or prompt changes.
|
||||||
|
*/
|
||||||
|
export function clearFormatCache() {
|
||||||
|
lastDetectedFormat = null;
|
||||||
|
formatCacheHits = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unwraps common envelope keys models may use around tracker payloads.
|
* Unwraps common envelope keys models may use around tracker payloads.
|
||||||
* Keeps extraction resilient when output is nested under wrappers like "trackers".
|
* Keeps extraction resilient when output is nested under wrappers like "trackers".
|
||||||
@@ -52,7 +146,7 @@ function unwrapTrackerEnvelope(payload) {
|
|||||||
* @returns {string} snake_case key from the base name only
|
* @returns {string} snake_case key from the base name only
|
||||||
*/
|
*/
|
||||||
function toFieldKey(name) {
|
function toFieldKey(name) {
|
||||||
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
const baseName = name.replace(/\s*\([^)]*\)\s*$/, '').trim();
|
||||||
return baseName
|
return baseName
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
@@ -70,10 +164,7 @@ function separateEmojiFromText(str) {
|
|||||||
|
|
||||||
str = str.trim();
|
str = str.trim();
|
||||||
|
|
||||||
// Regex to match emoji at the start (handles most emoji including compound ones)
|
const emojiMatch = str.match(EMOJI_RE);
|
||||||
// This matches emoji sequences including skin tones, gender modifiers, etc.
|
|
||||||
const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
|
|
||||||
const emojiMatch = str.match(emojiRegex);
|
|
||||||
|
|
||||||
if (emojiMatch) {
|
if (emojiMatch) {
|
||||||
const emoji = emojiMatch[0];
|
const emoji = emojiMatch[0];
|
||||||
@@ -121,13 +212,8 @@ function stripBrackets(text) {
|
|||||||
text = text.substring(1, text.length - 1).trim();
|
text = text.substring(1, text.length - 1).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove placeholder text patterns like [Location], [Mood Emoji], [Name], etc.
|
// Replace placeholders with empty string, keep real content
|
||||||
// Pattern matches: [anything with letters/spaces inside]
|
text = text.replace(PLACEHOLDER_PATTERN_RE, (match, content) => {
|
||||||
// This preserves actual content while removing template placeholders
|
|
||||||
const placeholderPattern = /\[([A-Za-z\s\/]+)\]/g;
|
|
||||||
|
|
||||||
// Check if a bracketed text looks like a placeholder vs real content
|
|
||||||
const isPlaceholder = (match, content) => {
|
|
||||||
// Common placeholder words to detect
|
// Common placeholder words to detect
|
||||||
const placeholderKeywords = [
|
const placeholderKeywords = [
|
||||||
'location', 'mood', 'emoji', 'name', 'description', 'placeholder',
|
'location', 'mood', 'emoji', 'name', 'description', 'placeholder',
|
||||||
@@ -141,23 +227,15 @@ function stripBrackets(text) {
|
|||||||
|
|
||||||
// If it contains common placeholder keywords, it's likely a placeholder
|
// If it contains common placeholder keywords, it's likely a placeholder
|
||||||
if (placeholderKeywords.some(keyword => lowerContent.includes(keyword))) {
|
if (placeholderKeywords.some(keyword => lowerContent.includes(keyword))) {
|
||||||
return true;
|
return ''; // Remove placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a short generic phrase (1-3 words) with only letters/spaces, might be placeholder
|
// If it's a short generic phrase (1-3 words) with only letters/spaces, might be placeholder
|
||||||
const wordCount = content.trim().split(/\s+/).length;
|
const wordCount = content.trim().split(/\s+/).length;
|
||||||
if (wordCount <= 3 && /^[A-Za-z\s\/]+$/.test(content)) {
|
if (wordCount <= 3 && /^[A-Za-z\s\/]+$/.test(content)) {
|
||||||
return true;
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replace placeholders with empty string, keep real content
|
|
||||||
text = text.replace(placeholderPattern, (match, content) => {
|
|
||||||
if (isPlaceholder(match, content)) {
|
|
||||||
return ''; // Remove placeholder
|
|
||||||
}
|
|
||||||
return match; // Keep real bracketed content
|
return match; // Keep real bracketed content
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,10 +271,11 @@ function debugLog(message, data = null) {
|
|||||||
* @param {string} responseText - The raw AI response text
|
* @param {string} responseText - The raw AI response text
|
||||||
* @param {Object} [options] - Parser behavior options
|
* @param {Object} [options] - Parser behavior options
|
||||||
* @param {boolean} [options.suppressNoDataError=false] - Avoid console error when no tracker data is found
|
* @param {boolean} [options.suppressNoDataError=false] - Avoid console error when no tracker data is found
|
||||||
|
* @param {boolean} [options.forceFormat=null] - Force a specific format check (bypasses cache)
|
||||||
* @returns {{userStats: string|null, infoBox: string|null, characterThoughts: string|null}} Parsed tracker data
|
* @returns {{userStats: string|null, infoBox: string|null, characterThoughts: string|null}} Parsed tracker data
|
||||||
*/
|
*/
|
||||||
export function parseResponse(responseText, options = {}) {
|
export function parseResponse(responseText, options = {}) {
|
||||||
const { suppressNoDataError = false } = options;
|
const { suppressNoDataError = false, forceFormat = null } = options;
|
||||||
const result = {
|
const result = {
|
||||||
userStats: null,
|
userStats: null,
|
||||||
infoBox: null,
|
infoBox: null,
|
||||||
@@ -210,14 +289,22 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
|
|
||||||
// Remove content inside thinking tags first (model's internal reasoning)
|
// Remove content inside thinking tags first (model's internal reasoning)
|
||||||
// This prevents parsing code blocks from the model's thinking process
|
// This prevents parsing code blocks from the model's thinking process
|
||||||
let cleanedResponse = responseText.replace(/<think>[\s\S]*?<\/think>/gi, '');
|
let cleanedResponse = responseText.replace(THINKING_TAG_RE, '');
|
||||||
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
|
cleanedResponse = cleanedResponse.replace(THINKING_TAG_ALT_RE, '');
|
||||||
debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars');
|
debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars');
|
||||||
|
|
||||||
// Remove "FORMAT:" markers that the model might accidentally output
|
// Remove "FORMAT:" markers that the model might accidentally output
|
||||||
cleanedResponse = cleanedResponse.replace(/FORMAT:\s*/gi, '');
|
cleanedResponse = cleanedResponse.replace(FORMAT_MARKER_RE, '');
|
||||||
debugLog('[RPG Parser] Removed FORMAT: markers, new length:', cleanedResponse.length + ' chars');
|
debugLog('[RPG Parser] Removed FORMAT: markers, new length:', cleanedResponse.length + ' chars');
|
||||||
|
|
||||||
|
// Format cache: if we've seen the same format multiple times, try that path first
|
||||||
|
const cachedFormat = forceFormat ?? lastDetectedFormat;
|
||||||
|
let detectedFormat = null;
|
||||||
|
|
||||||
|
if (cachedFormat && formatCacheHits >= FORMAT_CACHE_HIT_THRESHOLD) {
|
||||||
|
debugLog('[RPG Parser] Using cached format:', cachedFormat);
|
||||||
|
}
|
||||||
|
|
||||||
// First, try to extract raw JSON objects (v3 format)
|
// First, try to extract raw JSON objects (v3 format)
|
||||||
// Note: Prompts now instruct models to use ```json``` code blocks, but we extract
|
// Note: Prompts now instruct models to use ```json``` code blocks, but we extract
|
||||||
// from any JSON found using brace-matching for maximum compatibility
|
// from any JSON found using brace-matching for maximum compatibility
|
||||||
@@ -267,6 +354,15 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
// console.log(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
|
// console.log(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
|
||||||
debugLog(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
|
debugLog(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
|
||||||
|
|
||||||
|
// Update format cache
|
||||||
|
detectedFormat = 'json';
|
||||||
|
if (cachedFormat === 'json') {
|
||||||
|
formatCacheHits++;
|
||||||
|
} else {
|
||||||
|
lastDetectedFormat = 'json';
|
||||||
|
formatCacheHits = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// First, try to parse as unified JSON structure (new v3.1 format)
|
// First, try to parse as unified JSON structure (new v3.1 format)
|
||||||
// Look through all extracted objects for unified structure
|
// Look through all extracted objects for unified structure
|
||||||
let foundUnified = false;
|
let foundUnified = false;
|
||||||
@@ -377,13 +473,21 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
|
|
||||||
// Check for JSON code blocks (legacy v3 format with ```json fences)
|
// Check for JSON code blocks (legacy v3 format with ```json fences)
|
||||||
// Look for ```json code blocks which indicate JSON format
|
// Look for ```json code blocks which indicate JSON format
|
||||||
const jsonBlockRegex = /```json\s*\n([\s\S]*?)```/g;
|
const jsonMatches = [...cleanedResponse.matchAll(JSON_BLOCK_RE)];
|
||||||
const jsonMatches = [...cleanedResponse.matchAll(jsonBlockRegex)];
|
|
||||||
|
|
||||||
if (jsonMatches.length > 0) {
|
if (jsonMatches.length > 0) {
|
||||||
// console.log('[RPG Parser] ✓ Found', jsonMatches.length, 'JSON code blocks (v3 format with fences)');
|
// console.log('[RPG Parser] ✓ Found', jsonMatches.length, 'JSON code blocks (v3 format with fences)');
|
||||||
debugLog('[RPG Parser] ✓ Found JSON code blocks (v3 format), parsing as JSON');
|
debugLog('[RPG Parser] ✓ Found JSON code blocks (v3 format), parsing as JSON');
|
||||||
|
|
||||||
|
// Update format cache
|
||||||
|
detectedFormat = 'json_block';
|
||||||
|
if (cachedFormat === 'json_block') {
|
||||||
|
formatCacheHits++;
|
||||||
|
} else if (!detectedFormat) {
|
||||||
|
lastDetectedFormat = 'json_block';
|
||||||
|
formatCacheHits = 1;
|
||||||
|
}
|
||||||
|
|
||||||
for (let idx = 0; idx < jsonMatches.length; idx++) {
|
for (let idx = 0; idx < jsonMatches.length; idx++) {
|
||||||
const match = jsonMatches[idx];
|
const match = jsonMatches[idx];
|
||||||
const jsonContent = match[1].trim();
|
const jsonContent = match[1].trim();
|
||||||
@@ -447,13 +551,22 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if response uses XML <trackers> tags (hybrid format)
|
// Check if response uses XML <trackers> tags (hybrid format)
|
||||||
const xmlMatch = cleanedResponse.match(/<trackers>([\s\S]*?)<\/trackers>/i);
|
const xmlMatch = cleanedResponse.match(XML_TRACKERS_RE);
|
||||||
if (xmlMatch) {
|
if (xmlMatch) {
|
||||||
debugLog('[RPG Parser] ✓ Found XML <trackers> tags, using XML parser');
|
debugLog('[RPG Parser] ✓ Found XML <trackers> tags, using XML parser');
|
||||||
const trackersContent = xmlMatch[1].trim();
|
const trackersContent = xmlMatch[1].trim();
|
||||||
|
|
||||||
|
// Update format cache
|
||||||
|
detectedFormat = 'xml';
|
||||||
|
if (cachedFormat === 'xml') {
|
||||||
|
formatCacheHits++;
|
||||||
|
} else if (!lastDetectedFormat) {
|
||||||
|
lastDetectedFormat = 'xml';
|
||||||
|
formatCacheHits = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Try to parse JSON blocks within XML first
|
// Try to parse JSON blocks within XML first
|
||||||
const xmlJsonMatches = [...trackersContent.matchAll(jsonBlockRegex)];
|
const xmlJsonMatches = [...trackersContent.matchAll(JSON_BLOCK_RE)];
|
||||||
if (xmlJsonMatches.length > 0) {
|
if (xmlJsonMatches.length > 0) {
|
||||||
debugLog('[RPG Parser] Found JSON blocks within XML tags');
|
debugLog('[RPG Parser] Found JSON blocks within XML tags');
|
||||||
for (const match of xmlJsonMatches) {
|
for (const match of xmlJsonMatches) {
|
||||||
@@ -475,19 +588,19 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to text extraction from XML content (legacy v2 text format)
|
// Fallback to text extraction from XML content (legacy v2 text format)
|
||||||
const statsMatch = trackersContent.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
|
const statsMatch = trackersContent.match(XML_STATS_MATCH_RE);
|
||||||
if (statsMatch) {
|
if (statsMatch) {
|
||||||
result.userStats = stripBrackets(statsMatch[0].trim());
|
result.userStats = stripBrackets(statsMatch[0].trim());
|
||||||
debugLog('[RPG Parser] ✓ Extracted Stats from XML (text format)');
|
debugLog('[RPG Parser] ✓ Extracted Stats from XML (text format)');
|
||||||
}
|
}
|
||||||
|
|
||||||
const infoBoxMatch = trackersContent.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
|
const infoBoxMatch = trackersContent.match(XML_INFOBOX_MATCH_RE);
|
||||||
if (infoBoxMatch) {
|
if (infoBoxMatch) {
|
||||||
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
|
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
|
||||||
debugLog('[RPG Parser] ✓ Extracted Info Box from XML (text format)');
|
debugLog('[RPG Parser] ✓ Extracted Info Box from XML (text format)');
|
||||||
}
|
}
|
||||||
|
|
||||||
const charactersMatch = trackersContent.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
|
const charactersMatch = trackersContent.match(XML_CHARACTERS_MATCH_RE);
|
||||||
if (charactersMatch) {
|
if (charactersMatch) {
|
||||||
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
|
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
|
||||||
debugLog('[RPG Parser] ✓ Extracted Present Characters from XML (text format)');
|
debugLog('[RPG Parser] ✓ Extracted Present Characters from XML (text format)');
|
||||||
@@ -502,8 +615,7 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
debugLog('[RPG Parser] No XML tags found, using code block parser');
|
debugLog('[RPG Parser] No XML tags found, using code block parser');
|
||||||
|
|
||||||
// Extract code blocks
|
// Extract code blocks
|
||||||
const codeBlockRegex = /```([^`]+)```/g;
|
const matches = [...cleanedResponse.matchAll(CODE_BLOCK_RE)];
|
||||||
const matches = [...cleanedResponse.matchAll(codeBlockRegex)];
|
|
||||||
|
|
||||||
debugLog('[RPG Parser] Found', matches.length + ' code blocks');
|
debugLog('[RPG Parser] Found', matches.length + ' code blocks');
|
||||||
|
|
||||||
@@ -516,8 +628,8 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
|
|
||||||
// Check if this is a combined code block with multiple sections
|
// Check if this is a combined code block with multiple sections
|
||||||
const hasMultipleSections = (
|
const hasMultipleSections = (
|
||||||
content.match(/Stats\s*\n\s*---/i) &&
|
content.match(IS_STATS_HEADER_RE) &&
|
||||||
(content.match(/Info Box\s*\n\s*---/i) || content.match(/Present Characters\s*\n\s*---/i))
|
(content.match(IS_INFOBOX_HEADER_RE) || content.match(IS_CHARACTERS_HEADER_RE))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasMultipleSections) {
|
if (hasMultipleSections) {
|
||||||
@@ -525,21 +637,21 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
debugLog('[RPG Parser] ✓ Found combined code block with multiple sections');
|
debugLog('[RPG Parser] ✓ Found combined code block with multiple sections');
|
||||||
|
|
||||||
// Extract User Stats section
|
// Extract User Stats section
|
||||||
const statsMatch = content.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
|
const statsMatch = content.match(COMBINED_STATS_RE);
|
||||||
if (statsMatch && !result.userStats) {
|
if (statsMatch && !result.userStats) {
|
||||||
result.userStats = stripBrackets(statsMatch[0].trim());
|
result.userStats = stripBrackets(statsMatch[0].trim());
|
||||||
debugLog('[RPG Parser] ✓ Extracted Stats from combined block');
|
debugLog('[RPG Parser] ✓ Extracted Stats from combined block');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract Info Box section
|
// Extract Info Box section
|
||||||
const infoBoxMatch = content.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
|
const infoBoxMatch = content.match(COMBINED_INFOBOX_RE);
|
||||||
if (infoBoxMatch && !result.infoBox) {
|
if (infoBoxMatch && !result.infoBox) {
|
||||||
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
|
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
|
||||||
debugLog('[RPG Parser] ✓ Extracted Info Box from combined block');
|
debugLog('[RPG Parser] ✓ Extracted Info Box from combined block');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract Present Characters section
|
// Extract Present Characters section
|
||||||
const charactersMatch = content.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
|
const charactersMatch = content.match(COMBINED_CHARACTERS_RE);
|
||||||
if (charactersMatch && !result.characterThoughts) {
|
if (charactersMatch && !result.characterThoughts) {
|
||||||
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
|
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
|
||||||
debugLog('[RPG Parser] ✓ Extracted Present Characters from combined block');
|
debugLog('[RPG Parser] ✓ Extracted Present Characters from combined block');
|
||||||
@@ -548,27 +660,27 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
// Handle separate code blocks with flexible pattern matching
|
// Handle separate code blocks with flexible pattern matching
|
||||||
// Match Stats section - flexible patterns
|
// Match Stats section - flexible patterns
|
||||||
const isStats =
|
const isStats =
|
||||||
content.match(/Stats\s*\n\s*---/i) ||
|
content.match(IS_STATS_HEADER_RE) ||
|
||||||
content.match(/User Stats\s*\n\s*---/i) ||
|
content.match(IS_USER_STATS_HEADER_RE) ||
|
||||||
content.match(/Player Stats\s*\n\s*---/i) ||
|
content.match(IS_PLAYER_STATS_HEADER_RE) ||
|
||||||
// Fallback: look for stat keywords without strict header
|
// Fallback: look for stat keywords without strict header
|
||||||
(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i));
|
(content.match(IS_STATS_HEALTH_RE) && content.match(IS_STATS_ENERGY_RE));
|
||||||
|
|
||||||
// Match Info Box section - flexible patterns
|
// Match Info Box section - flexible patterns
|
||||||
const isInfoBox =
|
const isInfoBox =
|
||||||
content.match(/Info Box\s*\n\s*---/i) ||
|
content.match(IS_INFOBOX_HEADER_RE) ||
|
||||||
content.match(/Scene Info\s*\n\s*---/i) ||
|
content.match(IS_SCENE_INFO_HEADER_RE) ||
|
||||||
content.match(/Information\s*\n\s*---/i) ||
|
content.match(IS_INFORMATION_HEADER_RE) ||
|
||||||
// Fallback: look for info box keywords
|
// Fallback: look for info box keywords
|
||||||
(content.match(/Date:/i) && content.match(/Location:/i) && content.match(/Time:/i));
|
(content.match(IS_INFOBOX_DATE_RE) && content.match(IS_INFOBOX_LOCATION_RE) && content.match(IS_INFOBOX_TIME_RE));
|
||||||
|
|
||||||
// Match Present Characters section - flexible patterns
|
// Match Present Characters section - flexible patterns
|
||||||
const isCharacters =
|
const isCharacters =
|
||||||
content.match(/Present Characters\s*\n\s*---/i) ||
|
content.match(IS_CHARACTERS_HEADER_RE) ||
|
||||||
content.match(/Characters\s*\n\s*---/i) ||
|
content.match(IS_CHARACTERS_ALT_RE) ||
|
||||||
content.match(/Character Thoughts\s*\n\s*---/i) ||
|
content.match(IS_CHARACTERS_THOUGHTS_RE) ||
|
||||||
// Fallback: look for new multi-line format patterns
|
// Fallback: look for new multi-line format patterns
|
||||||
(content.match(/^-\s+\w+/m) && content.match(/Details:/i));
|
(content.match(IS_CHARACTERS_BULLET_RE) && content.match(IS_CHARACTERS_DETAILS_RE));
|
||||||
|
|
||||||
if (isStats && !result.userStats) {
|
if (isStats && !result.userStats) {
|
||||||
result.userStats = stripBrackets(content);
|
result.userStats = stripBrackets(content);
|
||||||
@@ -582,12 +694,12 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
debugLog('[RPG Parser] Full content:', content);
|
debugLog('[RPG Parser] Full content:', content);
|
||||||
} else {
|
} else {
|
||||||
debugLog('[RPG Parser] ✗ No match - checking patterns:');
|
debugLog('[RPG Parser] ✗ No match - checking patterns:');
|
||||||
debugLog('[RPG Parser] - Has "Stats\\n---"?', !!content.match(/Stats\s*\n\s*---/i));
|
debugLog('[RPG Parser] - Has "Stats\\n---"?', !!content.match(DEBUG_STATS_RE));
|
||||||
debugLog('[RPG Parser] - Has stat keywords?', !!(content.match(/Health:\s*\d+%/i) && content.match(/Energy:\s*\d+%/i)));
|
debugLog('[RPG Parser] - Has stat keywords?', !!(content.match(IS_STATS_HEALTH_RE) && content.match(IS_STATS_ENERGY_RE)));
|
||||||
debugLog('[RPG Parser] - Has "Info Box\\n---"?', !!content.match(/Info Box\s*\n\s*---/i));
|
debugLog('[RPG Parser] - Has "Info Box\\n---"?', !!content.match(DEBUG_INFOBOX_RE));
|
||||||
debugLog('[RPG Parser] - Has info keywords?', !!(content.match(/Date:/i) && content.match(/Location:/i)));
|
debugLog('[RPG Parser] - Has info keywords?', !!(content.match(IS_INFOBOX_DATE_RE) && content.match(IS_INFOBOX_LOCATION_RE)));
|
||||||
debugLog('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(/Present Characters\s*\n\s*---/i));
|
debugLog('[RPG Parser] - Has "Present Characters\\n---"?', !!content.match(DEBUG_CHARACTERS_RE));
|
||||||
debugLog('[RPG Parser] - Has new format ("- Name" + "Details:")?', !!(content.match(/^-\s+\w+/m) && content.match(/Details:/i)));
|
debugLog('[RPG Parser] - Has new format ("- Name" + "Details:")?', !!(content.match(IS_CHARACTERS_BULLET_RE) && content.match(IS_CHARACTERS_DETAILS_RE)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,8 +713,7 @@ export function parseResponse(responseText, options = {}) {
|
|||||||
// Final fallback: try to extract tracker JSON from any fenced block content
|
// Final fallback: try to extract tracker JSON from any fenced block content
|
||||||
// This catches responses where JSON is embedded in non-standard markdown structure.
|
// This catches responses where JSON is embedded in non-standard markdown structure.
|
||||||
if (!result.userStats && !result.infoBox && !result.characterThoughts) {
|
if (!result.userStats && !result.infoBox && !result.characterThoughts) {
|
||||||
const fencedRegex = /```(?:json)?\s*\n?([\s\S]*?)```/gi;
|
const fencedMatches = [...cleanedResponse.matchAll(FENCED_FALLBACK_RE)];
|
||||||
const fencedMatches = [...cleanedResponse.matchAll(fencedRegex)];
|
|
||||||
|
|
||||||
for (const match of fencedMatches) {
|
for (const match of fencedMatches) {
|
||||||
const fencedContent = (match[1] || '').trim();
|
const fencedContent = (match[1] || '').trim();
|
||||||
@@ -806,13 +917,13 @@ export function parseUserStats(statsText) {
|
|||||||
|
|
||||||
// Parse RPG attributes if enabled
|
// Parse RPG attributes if enabled
|
||||||
if (trackerConfig?.userStats?.showRPGAttributes) {
|
if (trackerConfig?.userStats?.showRPGAttributes) {
|
||||||
const strMatch = statsText.match(/STR:\s*(\d+)/i);
|
const strMatch = statsText.match(RPG_STR_RE);
|
||||||
const dexMatch = statsText.match(/DEX:\s*(\d+)/i);
|
const dexMatch = statsText.match(RPG_DEX_RE);
|
||||||
const conMatch = statsText.match(/CON:\s*(\d+)/i);
|
const conMatch = statsText.match(RPG_CON_RE);
|
||||||
const intMatch = statsText.match(/INT:\s*(\d+)/i);
|
const intMatch = statsText.match(RPG_INT_RE);
|
||||||
const wisMatch = statsText.match(/WIS:\s*(\d+)/i);
|
const wisMatch = statsText.match(RPG_WIS_RE);
|
||||||
const chaMatch = statsText.match(/CHA:\s*(\d+)/i);
|
const chaMatch = statsText.match(RPG_CHA_RE);
|
||||||
const lvlMatch = statsText.match(/LVL:\s*(\d+)/i);
|
const lvlMatch = statsText.match(RPG_LVL_RE);
|
||||||
|
|
||||||
if (strMatch) extensionSettings.classicStats.str = parseInt(strMatch[1]);
|
if (strMatch) extensionSettings.classicStats.str = parseInt(strMatch[1]);
|
||||||
if (dexMatch) extensionSettings.classicStats.dex = parseInt(dexMatch[1]);
|
if (dexMatch) extensionSettings.classicStats.dex = parseInt(dexMatch[1]);
|
||||||
@@ -832,7 +943,7 @@ export function parseUserStats(statsText) {
|
|||||||
const customFields = statusConfig.customFields || [];
|
const customFields = statusConfig.customFields || [];
|
||||||
|
|
||||||
// Try Status: format
|
// Try Status: format
|
||||||
const statusMatch = statsText.match(/Status:\s*(.+)/i);
|
const statusMatch = statsText.match(STATUS_MATCH_RE);
|
||||||
if (statusMatch) {
|
if (statusMatch) {
|
||||||
const statusContent = statusMatch[1].trim();
|
const statusContent = statusMatch[1].trim();
|
||||||
|
|
||||||
@@ -883,7 +994,7 @@ export function parseUserStats(statsText) {
|
|||||||
// Parse skills section if enabled
|
// Parse skills section if enabled
|
||||||
const skillsConfig = trackerConfig?.userStats?.skillsSection;
|
const skillsConfig = trackerConfig?.userStats?.skillsSection;
|
||||||
if (skillsConfig?.enabled) {
|
if (skillsConfig?.enabled) {
|
||||||
const skillsMatch = statsText.match(/Skills:\s*(.+)/i);
|
const skillsMatch = statsText.match(SKILLS_MATCH_RE);
|
||||||
if (skillsMatch) {
|
if (skillsMatch) {
|
||||||
extensionSettings.userStats.skills = skillsMatch[1].trim();
|
extensionSettings.userStats.skills = skillsMatch[1].trim();
|
||||||
debugLog('[RPG Parser] Skills extracted:', skillsMatch[1].trim());
|
debugLog('[RPG Parser] Skills extracted:', skillsMatch[1].trim());
|
||||||
@@ -901,7 +1012,7 @@ export function parseUserStats(statsText) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy v1 parsing for backward compatibility
|
// Legacy v1 parsing for backward compatibility
|
||||||
const inventoryMatch = statsText.match(/Inventory:\s*(.+)/i);
|
const inventoryMatch = statsText.match(INVENTORY_MATCH_RE);
|
||||||
if (inventoryMatch) {
|
if (inventoryMatch) {
|
||||||
extensionSettings.userStats.inventory = inventoryMatch[1].trim();
|
extensionSettings.userStats.inventory = inventoryMatch[1].trim();
|
||||||
debugLog('[RPG Parser] Inventory v1 extracted:', inventoryMatch[1].trim());
|
debugLog('[RPG Parser] Inventory v1 extracted:', inventoryMatch[1].trim());
|
||||||
@@ -911,13 +1022,13 @@ export function parseUserStats(statsText) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract quests
|
// Extract quests
|
||||||
const mainQuestMatch = statsText.match(/Main Quests?:\s*(.+)/i);
|
const mainQuestMatch = statsText.match(MAIN_QUEST_MATCH_RE);
|
||||||
if (mainQuestMatch) {
|
if (mainQuestMatch) {
|
||||||
extensionSettings.quests.main = mainQuestMatch[1].trim();
|
extensionSettings.quests.main = mainQuestMatch[1].trim();
|
||||||
debugLog('[RPG Parser] Main quests extracted:', mainQuestMatch[1].trim());
|
debugLog('[RPG Parser] Main quests extracted:', mainQuestMatch[1].trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionalQuestsMatch = statsText.match(/Optional Quests:\s*(.+)/i);
|
const optionalQuestsMatch = statsText.match(OPTIONAL_QUESTS_MATCH_RE);
|
||||||
if (optionalQuestsMatch) {
|
if (optionalQuestsMatch) {
|
||||||
const questsText = optionalQuestsMatch[1].trim();
|
const questsText = optionalQuestsMatch[1].trim();
|
||||||
if (questsText && questsText !== 'None') {
|
if (questsText && questsText !== 'None') {
|
||||||
@@ -960,8 +1071,7 @@ export function parseUserStats(statsText) {
|
|||||||
* @returns {Array<string>} Array of code block contents
|
* @returns {Array<string>} Array of code block contents
|
||||||
*/
|
*/
|
||||||
export function extractCodeBlocks(text) {
|
export function extractCodeBlocks(text) {
|
||||||
const codeBlockRegex = /```([^`]+)```/g;
|
const matches = [...text.matchAll(CODE_BLOCK_RE)];
|
||||||
const matches = [...text.matchAll(codeBlockRegex)];
|
|
||||||
return matches.map(match => match[1].trim());
|
return matches.map(match => match[1].trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,7 +1081,7 @@ export function extractCodeBlocks(text) {
|
|||||||
* @returns {boolean} True if this is a stats section
|
* @returns {boolean} True if this is a stats section
|
||||||
*/
|
*/
|
||||||
export function isStatsSection(content) {
|
export function isStatsSection(content) {
|
||||||
return content.match(/Stats\s*\n\s*---/i) !== null;
|
return content.match(STATS_SECTION_RE) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -980,7 +1090,7 @@ export function isStatsSection(content) {
|
|||||||
* @returns {boolean} True if this is an info box section
|
* @returns {boolean} True if this is an info box section
|
||||||
*/
|
*/
|
||||||
export function isInfoBoxSection(content) {
|
export function isInfoBoxSection(content) {
|
||||||
return content.match(/Info Box\s*\n\s*---/i) !== null;
|
return content.match(INFOBOX_SECTION_RE) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -989,5 +1099,5 @@ export function isInfoBoxSection(content) {
|
|||||||
* @returns {boolean} True if this is a character thoughts section
|
* @returns {boolean} True if this is a character thoughts section
|
||||||
*/
|
*/
|
||||||
export function isCharacterThoughtsSection(content) {
|
export function isCharacterThoughtsSection(content) {
|
||||||
return content.match(/Present Characters\s*\n\s*---/i) !== null || content.includes(" | ");
|
return content.match(CHARACTERS_SECTION_RE) !== null || content.includes(" | ");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js';
|
import { chat, characters, this_chid } from '../../../../../../../script.js';
|
||||||
import { selected_group, getGroupMembers, getGroupChat, groups } from '../../../../../../group-chats.js';
|
import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js';
|
||||||
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
|
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
|
||||||
import {
|
import {
|
||||||
buildUserStatsJSONInstruction,
|
buildUserStatsJSONInstruction,
|
||||||
@@ -78,85 +78,99 @@ async function getCharacterCardsInfo() {
|
|||||||
// Narrator mode: use character card as narrator context, infer characters from story context
|
// Narrator mode: use character card as narrator context, infer characters from story context
|
||||||
if (extensionSettings.narratorMode) {
|
if (extensionSettings.narratorMode) {
|
||||||
if (this_chid !== undefined && characters && characters[this_chid]) {
|
if (this_chid !== undefined && characters && characters[this_chid]) {
|
||||||
const character = characters[this_chid];
|
characterInfo = buildNarratorCardInfo(characters[this_chid]);
|
||||||
characterInfo += 'You are acting as the narrator for this story. The narrator card provides context for the story tone and style:\n\n';
|
|
||||||
characterInfo += `<narrator>\n`;
|
|
||||||
|
|
||||||
if (character.description) {
|
|
||||||
characterInfo += `${character.description}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (character.personality) {
|
|
||||||
characterInfo += `${character.personality}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
characterInfo += `</narrator>\n\n`;
|
|
||||||
|
|
||||||
// Use custom narrator prompt if available, otherwise use default
|
|
||||||
const narratorPrompt = extensionSettings.customNarratorPrompt || DEFAULT_NARRATOR_PROMPT;
|
|
||||||
characterInfo += narratorPrompt + '\n\n';
|
|
||||||
}
|
}
|
||||||
return characterInfo;
|
return characterInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if in group chat
|
// Check if in group chat
|
||||||
if (selected_group) {
|
if (selected_group) {
|
||||||
// Find the current group directly from the groups array
|
|
||||||
const group = groups.find(g => g.id === selected_group);
|
|
||||||
const groupMembers = getGroupMembers(selected_group);
|
const groupMembers = getGroupMembers(selected_group);
|
||||||
|
|
||||||
if (groupMembers && groupMembers.length > 0) {
|
if (groupMembers && groupMembers.length > 0) {
|
||||||
characterInfo += 'Characters in this roleplay:\n\n';
|
const disabledMembers = groups.find(g => g.id === selected_group)?.disabled_members || [];
|
||||||
|
characterInfo = buildGroupCardInfo(groupMembers, disabledMembers);
|
||||||
// Filter out disabled (muted) members
|
|
||||||
const disabledMembers = group?.disabled_members || [];
|
|
||||||
// console.log('[RPG Companion] 🔍 Group ID:', selected_group, '| Disabled members:', disabledMembers);
|
|
||||||
let characterIndex = 0;
|
|
||||||
|
|
||||||
groupMembers.forEach((member) => {
|
|
||||||
if (!member || !member.name) return;
|
|
||||||
|
|
||||||
// Skip muted characters - check against avatar filename
|
|
||||||
if (member.avatar && disabledMembers.includes(member.avatar)) {
|
|
||||||
// console.log(`[RPG Companion] ❌ Skipping muted: ${member.name} (${member.avatar})`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
characterIndex++;
|
|
||||||
characterInfo += `<character${characterIndex}="${member.name}">\n`;
|
|
||||||
|
|
||||||
if (member.description) {
|
|
||||||
characterInfo += `${member.description}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (member.personality) {
|
|
||||||
characterInfo += `${member.personality}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
characterInfo += `</character${characterIndex}>\n\n`;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (this_chid !== undefined && characters && characters[this_chid]) {
|
} else if (this_chid !== undefined && characters && characters[this_chid]) {
|
||||||
// Single character chat
|
// Single character chat
|
||||||
const character = characters[this_chid];
|
characterInfo = buildSingleCardInfo(characters[this_chid]);
|
||||||
|
|
||||||
characterInfo += 'Character in this roleplay:\n\n';
|
|
||||||
characterInfo += `<character="${character.name}">\n`;
|
|
||||||
|
|
||||||
if (character.description) {
|
|
||||||
characterInfo += `${character.description}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (character.personality) {
|
|
||||||
characterInfo += `${character.personality}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
characterInfo += `</character>\n\n`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return characterInfo;
|
return characterInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds narrator mode character card information.
|
||||||
|
* @param {Object} character - Character card data
|
||||||
|
* @returns {string} Formatted narrator info
|
||||||
|
*/
|
||||||
|
function buildNarratorCardInfo(character) {
|
||||||
|
let info = '';
|
||||||
|
info += 'You are acting as the narrator for this story. The narrator card provides context for the story tone and style:\n\n';
|
||||||
|
info += `<narrator>\n`;
|
||||||
|
info += appendCharacterFields(character);
|
||||||
|
info += `</narrator>\n\n`;
|
||||||
|
info += (extensionSettings.customNarratorPrompt || DEFAULT_NARRATOR_PROMPT) + '\n\n';
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds character card information for a group chat.
|
||||||
|
* @param {Array} groupMembers - Array of group member objects
|
||||||
|
* @param {Array} disabledMembers - Array of disabled (muted) member avatar filenames
|
||||||
|
* @returns {string} Formatted group character info
|
||||||
|
*/
|
||||||
|
function buildGroupCardInfo(groupMembers, disabledMembers) {
|
||||||
|
let info = 'Characters in this roleplay:\n\n';
|
||||||
|
let characterIndex = 0;
|
||||||
|
|
||||||
|
groupMembers.forEach((member) => {
|
||||||
|
if (!member || !member.name) return;
|
||||||
|
|
||||||
|
// Skip muted characters
|
||||||
|
if (member.avatar && disabledMembers.includes(member.avatar)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
characterIndex++;
|
||||||
|
info += `<character${characterIndex}="${member.name}">\n`;
|
||||||
|
info += appendCharacterFields(member);
|
||||||
|
info += `</character${characterIndex}>\n\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds character card information for a single character chat.
|
||||||
|
* @param {Object} character - Character card data
|
||||||
|
* @returns {string} Formatted single character info
|
||||||
|
*/
|
||||||
|
function buildSingleCardInfo(character) {
|
||||||
|
let info = 'Character in this roleplay:\n\n';
|
||||||
|
info += `<character="${character.name}">\n`;
|
||||||
|
info += appendCharacterFields(character);
|
||||||
|
info += `</character>\n\n`;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends description and personality fields from a character object.
|
||||||
|
* Shared helper used by narrator, group, and single character info builders.
|
||||||
|
* @param {Object} character - Character object with optional description/personality fields
|
||||||
|
* @returns {string} Appended fields (empty string if neither present)
|
||||||
|
*/
|
||||||
|
function appendCharacterFields(character) {
|
||||||
|
let fields = '';
|
||||||
|
if (character.description) {
|
||||||
|
fields += `${character.description}\n`;
|
||||||
|
}
|
||||||
|
if (character.personality) {
|
||||||
|
fields += `${character.personality}\n`;
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a formatted inventory summary for AI context injection.
|
* Builds a formatted inventory summary for AI context injection.
|
||||||
* Converts v2 inventory structure to multi-line plaintext format.
|
* Converts v2 inventory structure to multi-line plaintext format.
|
||||||
@@ -1232,7 +1246,6 @@ export async function generateSeparateUpdatePrompt() {
|
|||||||
// /hide command automatically handles checkpoint filtering
|
// /hide command automatically handles checkpoint filtering
|
||||||
// Add chat history as separate user/assistant messages with per-message historical context
|
// Add chat history as separate user/assistant messages with per-message historical context
|
||||||
const recentMessages = chat.slice(-depth);
|
const recentMessages = chat.slice(-depth);
|
||||||
const startIndex = chat.length - depth;
|
|
||||||
const position = historyPersistence?.injectionPosition || 'assistant_message_end';
|
const position = historyPersistence?.injectionPosition || 'assistant_message_end';
|
||||||
|
|
||||||
// Build a map of which messages should get context based on position setting
|
// Build a map of which messages should get context based on position setting
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function evaluateSuppression(extensionSettings, context, data) {
|
|||||||
let instructContent = '';
|
let instructContent = '';
|
||||||
if (instructObj) {
|
if (instructObj) {
|
||||||
if (typeof instructObj === 'object') {
|
if (typeof instructObj === 'object') {
|
||||||
instructContent = String(instructObj.value || instructObj || '');
|
instructContent = String(instructObj.value || '');
|
||||||
} else {
|
} else {
|
||||||
instructContent = String(instructObj);
|
instructContent = String(instructObj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { chat, chat_metadata, user_avatar, setExtensionPrompt, extension_prompt_types } from '../../../../../../../script.js';
|
import { chat, chat_metadata, user_avatar, setExtensionPrompt, extension_prompt_types, updateMessageBlock } from '../../../../../../../script.js';
|
||||||
|
|
||||||
// Core modules
|
// Core modules
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||||
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
|
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
|
||||||
import { buildInventorySummary } from '../generation/promptBuilder.js';
|
|
||||||
import { buildUserStatsText } from '../rendering/userStats.js';
|
import { buildUserStatsText } from '../rendering/userStats.js';
|
||||||
import { renderInventory, getLocationId } from '../rendering/inventory.js';
|
import { renderInventory, getLocationId } from '../rendering/inventory.js';
|
||||||
import { parseItems, serializeItems } from '../../utils/itemParser.js';
|
import { parseItems, serializeItems } from '../../utils/itemParser.js';
|
||||||
|
|||||||
@@ -146,10 +146,29 @@ function generateEquipmentHTML() {
|
|||||||
* Gets data from state/settings and updates DOM directly.
|
* Gets data from state/settings and updates DOM directly.
|
||||||
*/
|
*/
|
||||||
export function renderEquipment() {
|
export function renderEquipment() {
|
||||||
|
// Ensure showEquipment defaults to true if undefined
|
||||||
|
if (extensionSettings.showEquipment === undefined) {
|
||||||
|
extensionSettings.showEquipment = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$equipmentContainer || !extensionSettings.showEquipment) {
|
if (!$equipmentContainer || !extensionSettings.showEquipment) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure equipment data structure exists (defensive initialization)
|
||||||
|
if (!extensionSettings.userStats?.equipment) {
|
||||||
|
extensionSettings.userStats = extensionSettings.userStats || {};
|
||||||
|
extensionSettings.userStats.equipment = {
|
||||||
|
items: [],
|
||||||
|
slots: {
|
||||||
|
helmet: null, ring1: null, ring2: null, ring3: null, ring4: null,
|
||||||
|
ring5: null, ring6: null, ring7: null, ring8: null, ring9: null, ring10: null,
|
||||||
|
necklace: null, bodyArmor: null, pants: null, shoes: null, gloves: null,
|
||||||
|
accessory1: null, accessory2: null, accessory3: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const html = generateEquipmentHTML();
|
const html = generateEquipmentHTML();
|
||||||
updateIfChanged($equipmentContainer, html, 'rpg-equipment');
|
updateIfChanged($equipmentContainer, html, 'rpg-equipment');
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
$infoBoxContainer
|
$infoBoxContainer
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData, setMessageSwipeTrackerField } from '../../core/persistence.js';
|
import { saveChatData, saveSettings, setMessageSwipeTrackerField } from '../../core/persistence.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { isItemLocked } from '../generation/lockManager.js';
|
import { isItemLocked } from '../generation/lockManager.js';
|
||||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
|
|||||||
@@ -645,7 +645,10 @@ export function renderInventory() {
|
|||||||
*/
|
*/
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
const div = document.createElement('div');
|
return String(text)
|
||||||
div.textContent = text;
|
.replace(/&/g, '&')
|
||||||
return div.innerHTML;
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,9 +57,13 @@ function getLockIconHtml(tracker, path) {
|
|||||||
* @returns {string} Escaped HTML
|
* @returns {string} Escaped HTML
|
||||||
*/
|
*/
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
if (!text) return '';
|
||||||
div.textContent = text;
|
return String(text)
|
||||||
return div.innerHTML;
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -131,21 +131,6 @@ export function clearAllCaches() {
|
|||||||
htmlCache.clear();
|
htmlCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps an existing render function with change detection.
|
|
||||||
* The wrapped function generates HTML, compares it to the cache,
|
|
||||||
* and only updates the DOM if content changed.
|
|
||||||
*
|
|
||||||
* @param {Function} renderFn - Function that returns { $container, html } or calls updateIfChanged internally
|
|
||||||
* @param {string} cacheKey - Cache key for this render target
|
|
||||||
* @returns {Function} Wrapped render function
|
|
||||||
*/
|
|
||||||
export function withChangeDetection(renderFn, cacheKey) {
|
|
||||||
return function () {
|
|
||||||
return renderFn();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batches multiple DOM updates into a single reflow by using
|
* Batches multiple DOM updates into a single reflow by using
|
||||||
* document fragment pattern. Reduces layout thrashing.
|
* document fragment pattern. Reduces layout thrashing.
|
||||||
|
|||||||
@@ -1301,30 +1301,6 @@ export function updateCharacterField(characterName, field, value) {
|
|||||||
// Note: Don't call renderThoughts() here - it would overwrite the user's edits
|
// Note: Don't call renderThoughts() here - it would overwrite the user's edits
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders only the sidebar thoughts panel without updating chat bubbles
|
|
||||||
*/
|
|
||||||
function renderThoughtsSidebarOnly() {
|
|
||||||
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a simplified version that only updates the sidebar
|
|
||||||
// Copy the rendering logic from renderThoughts but skip the updateChatThoughts call
|
|
||||||
const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts;
|
|
||||||
if (!thoughtsData) {
|
|
||||||
$thoughtsContainer.html('<div class="rpg-inventory-empty">' + (i18n.getTranslation('thoughts.empty') || 'No character data generated yet') + '</div>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-render sidebar content (this would be the full logic from renderThoughts)
|
|
||||||
// For now, just call renderThoughts but set a flag
|
|
||||||
const originalShowInChat = extensionSettings.showThoughtsInChat;
|
|
||||||
extensionSettings.showThoughtsInChat = false;
|
|
||||||
renderThoughts();
|
|
||||||
extensionSettings.showThoughtsInChat = originalShowInChat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates or removes thoughts shown in chat.
|
* Updates or removes thoughts shown in chat.
|
||||||
* Renders either the original corner bubbles or inline dropdown cards.
|
* Renders either the original corner bubbles or inline dropdown cards.
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ function updateUserStatsData() {
|
|||||||
if (!itemString) return [];
|
if (!itemString) return [];
|
||||||
const items = itemString.split(',').map(s => s.trim()).filter(s => s);
|
const items = itemString.split(',').map(s => s.trim()).filter(s => s);
|
||||||
return items.map(item => {
|
return items.map(item => {
|
||||||
const qtyMatch = item.match(/^(\\d+)x\\s+(.+)$/);
|
const qtyMatch = item.match(/^(\d+)x\s+(.+)$/);
|
||||||
if (qtyMatch) {
|
if (qtyMatch) {
|
||||||
return { name: qtyMatch[2].trim(), quantity: parseInt(qtyMatch[1]) };
|
return { name: qtyMatch[2].trim(), quantity: parseInt(qtyMatch[1]) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,31 +339,4 @@ function processExpandedButton(messageBlock) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the checkpoint button in an existing menu
|
|
||||||
* @param {HTMLElement} menu - The extraMesButtons or mes_buttons container
|
|
||||||
* @param {number} messageId - The message index
|
|
||||||
*/
|
|
||||||
function updateCheckpointButtonInMenu(menu, messageId) {
|
|
||||||
if (!menu) return;
|
|
||||||
|
|
||||||
// Find the checkpoint button (either dropdown or expanded)
|
|
||||||
const existingButton = menu.querySelector('.rpg-checkpoint-button, .rpg-checkpoint-button-expanded');
|
|
||||||
if (!existingButton) return;
|
|
||||||
|
|
||||||
const isCheckpoint = isCheckpointMessage(messageId);
|
|
||||||
|
|
||||||
// Update icon
|
|
||||||
const icon = existingButton.querySelector('i');
|
|
||||||
if (icon) {
|
|
||||||
icon.className = isCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark';
|
|
||||||
icon.style.color = isCheckpoint ? '#4a9eff' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update tooltip
|
|
||||||
existingButton.title = isCheckpoint
|
|
||||||
? 'Clear Chapter Start'
|
|
||||||
: 'Set Chapter Start — When bookmarked, this message will count as the first message in the chat history, skipping earlier ones.';
|
|
||||||
const translationKey = isCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart';
|
|
||||||
existingButton.setAttribute('data-i18n', translationKey);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||||
import { hexToRgba } from './theme.js';
|
import { hexToRgba } from './theme.js';
|
||||||
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to parse time string and calculate clock hand angles
|
* Helper to parse time string and calculate clock hand angles
|
||||||
@@ -50,7 +51,7 @@ export function updateStripWidgets() {
|
|||||||
let infoData = null;
|
let infoData = null;
|
||||||
if (infoBox) {
|
if (infoBox) {
|
||||||
try {
|
try {
|
||||||
infoData = typeof infoBox === 'string' ? JSON.parse(infoBox) : infoBox;
|
infoData = typeof infoBox === 'string' ? repairJSON(infoBox) : infoBox;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[RPG Strip Widgets] Failed to parse infoBox:', e);
|
console.warn('[RPG Strip Widgets] Failed to parse infoBox:', e);
|
||||||
}
|
}
|
||||||
@@ -124,7 +125,7 @@ export function updateStripWidgets() {
|
|||||||
const userStatsData = lastGeneratedData?.userStats || committedTrackerData?.userStats;
|
const userStatsData = lastGeneratedData?.userStats || committedTrackerData?.userStats;
|
||||||
if (userStatsData) {
|
if (userStatsData) {
|
||||||
try {
|
try {
|
||||||
const parsedStats = typeof userStatsData === 'string' ? JSON.parse(userStatsData) : userStatsData;
|
const parsedStats = typeof userStatsData === 'string' ? repairJSON(userStatsData) : userStatsData;
|
||||||
if (parsedStats?.stats) {
|
if (parsedStats?.stats) {
|
||||||
allStats = parsedStats.stats;
|
allStats = parsedStats.stats;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-435
@@ -9,6 +9,7 @@ import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layou
|
|||||||
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
|
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { hexToRgba } from './theme.js';
|
import { hexToRgba } from './theme.js';
|
||||||
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the text labels of the mobile navigation tabs based on the current language.
|
* Updates the text labels of the mobile navigation tabs based on the current language.
|
||||||
@@ -503,7 +504,10 @@ export function setupMobileToggle() {
|
|||||||
// top: $panel.css('top'),
|
// top: $panel.css('top'),
|
||||||
// bottom: $panel.css('top'),
|
// bottom: $panel.css('top'),
|
||||||
// transform: $panel.css('transform'),
|
// transform: $panel.css('transform'),
|
||||||
// visibility: $panel.css('visibility')\n // }\n // });\n setupMobileTabs();
|
// visibility: $panel.css('visibility')
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
setupMobileTabs();
|
||||||
// Set initial icon for mobile
|
// Set initial icon for mobile
|
||||||
updateCollapseToggleIcon();
|
updateCollapseToggleIcon();
|
||||||
// Show mobile toggle on mobile viewport
|
// Show mobile toggle on mobile viewport
|
||||||
@@ -860,437 +864,6 @@ export function setupContentEditableScrolling() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up the mobile refresh button with drag functionality.
|
|
||||||
* Same pattern as mobile toggle button.
|
|
||||||
* Tap = refresh, drag = reposition
|
|
||||||
*/
|
|
||||||
export function setupRefreshButtonDrag() {
|
|
||||||
const $refreshBtn = $('#rpg-manual-update-mobile');
|
|
||||||
|
|
||||||
if ($refreshBtn.length === 0) {
|
|
||||||
console.warn('[RPG Mobile] Refresh button not found in DOM');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('[RPG Mobile] setupRefreshButtonDrag called');
|
|
||||||
|
|
||||||
// Load and apply saved position
|
|
||||||
if (extensionSettings.mobileRefreshPosition) {
|
|
||||||
const pos = extensionSettings.mobileRefreshPosition;
|
|
||||||
// console.log('[RPG Mobile] Loading saved refresh button position:', pos);
|
|
||||||
|
|
||||||
// Apply saved position
|
|
||||||
if (pos.top) $refreshBtn.css('top', pos.top);
|
|
||||||
if (pos.right) $refreshBtn.css('right', pos.right);
|
|
||||||
if (pos.bottom) $refreshBtn.css('bottom', pos.bottom);
|
|
||||||
if (pos.left) $refreshBtn.css('left', pos.left);
|
|
||||||
|
|
||||||
// Constrain to viewport after position is applied
|
|
||||||
requestAnimationFrame(() => constrainFabToViewport($refreshBtn));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Touch/drag state
|
|
||||||
let isDragging = false;
|
|
||||||
let touchStartTime = 0;
|
|
||||||
let touchStartX = 0;
|
|
||||||
let touchStartY = 0;
|
|
||||||
let buttonStartX = 0;
|
|
||||||
let buttonStartY = 0;
|
|
||||||
const LONG_PRESS_DURATION = 200;
|
|
||||||
const MOVE_THRESHOLD = 10;
|
|
||||||
let rafId = null;
|
|
||||||
let pendingX = null;
|
|
||||||
let pendingY = null;
|
|
||||||
|
|
||||||
// Update position using requestAnimationFrame
|
|
||||||
function updatePosition() {
|
|
||||||
if (pendingX !== null && pendingY !== null) {
|
|
||||||
$refreshBtn.css({
|
|
||||||
left: pendingX + 'px',
|
|
||||||
top: pendingY + 'px',
|
|
||||||
right: 'auto',
|
|
||||||
bottom: 'auto'
|
|
||||||
});
|
|
||||||
pendingX = null;
|
|
||||||
pendingY = null;
|
|
||||||
}
|
|
||||||
rafId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Touch start
|
|
||||||
$refreshBtn.on('touchstart', function(e) {
|
|
||||||
const touch = e.originalEvent.touches[0];
|
|
||||||
touchStartTime = Date.now();
|
|
||||||
touchStartX = touch.clientX;
|
|
||||||
touchStartY = touch.clientY;
|
|
||||||
|
|
||||||
const offset = $refreshBtn.offset();
|
|
||||||
buttonStartX = offset.left;
|
|
||||||
buttonStartY = offset.top;
|
|
||||||
|
|
||||||
isDragging = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Touch move
|
|
||||||
$refreshBtn.on('touchmove', function(e) {
|
|
||||||
const touch = e.originalEvent.touches[0];
|
|
||||||
const deltaX = touch.clientX - touchStartX;
|
|
||||||
const deltaY = touch.clientY - touchStartY;
|
|
||||||
const timeSinceStart = Date.now() - touchStartTime;
|
|
||||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
||||||
|
|
||||||
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
|
||||||
isDragging = true;
|
|
||||||
$refreshBtn.addClass('dragging');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDragging) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
let newX = buttonStartX + deltaX;
|
|
||||||
let newY = buttonStartY + deltaY;
|
|
||||||
|
|
||||||
const buttonWidth = $refreshBtn.outerWidth();
|
|
||||||
const buttonHeight = $refreshBtn.outerHeight();
|
|
||||||
|
|
||||||
const minX = 10;
|
|
||||||
const maxX = window.innerWidth - buttonWidth - 10;
|
|
||||||
const minY = 10;
|
|
||||||
const maxY = window.innerHeight - buttonHeight - 10;
|
|
||||||
|
|
||||||
newX = Math.max(minX, Math.min(maxX, newX));
|
|
||||||
newY = Math.max(minY, Math.min(maxY, newY));
|
|
||||||
|
|
||||||
pendingX = newX;
|
|
||||||
pendingY = newY;
|
|
||||||
if (!rafId) {
|
|
||||||
rafId = requestAnimationFrame(updatePosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Touch end
|
|
||||||
$refreshBtn.on('touchend', function(e) {
|
|
||||||
if (isDragging) {
|
|
||||||
// Save new position
|
|
||||||
const offset = $refreshBtn.offset();
|
|
||||||
const newPosition = {
|
|
||||||
left: offset.left + 'px',
|
|
||||||
top: offset.top + 'px'
|
|
||||||
};
|
|
||||||
|
|
||||||
extensionSettings.mobileRefreshPosition = newPosition;
|
|
||||||
saveSettings();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
$refreshBtn.removeClass('dragging');
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
// Set flag to prevent click handler from firing
|
|
||||||
$refreshBtn.data('just-dragged', true);
|
|
||||||
setTimeout(() => {
|
|
||||||
$refreshBtn.data('just-dragged', false);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
isDragging = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mouse support for desktop
|
|
||||||
let mouseDown = false;
|
|
||||||
|
|
||||||
$refreshBtn.on('mousedown', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
touchStartTime = Date.now();
|
|
||||||
touchStartX = e.clientX;
|
|
||||||
touchStartY = e.clientY;
|
|
||||||
|
|
||||||
const offset = $refreshBtn.offset();
|
|
||||||
buttonStartX = offset.left;
|
|
||||||
buttonStartY = offset.top;
|
|
||||||
|
|
||||||
mouseDown = true;
|
|
||||||
isDragging = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on('mousemove', function(e) {
|
|
||||||
if (!mouseDown) return;
|
|
||||||
|
|
||||||
const deltaX = e.clientX - touchStartX;
|
|
||||||
const deltaY = e.clientY - touchStartY;
|
|
||||||
const timeSinceStart = Date.now() - touchStartTime;
|
|
||||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
||||||
|
|
||||||
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
|
||||||
isDragging = true;
|
|
||||||
$refreshBtn.addClass('dragging');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDragging) {
|
|
||||||
let newX = buttonStartX + deltaX;
|
|
||||||
let newY = buttonStartY + deltaY;
|
|
||||||
|
|
||||||
const buttonWidth = $refreshBtn.outerWidth();
|
|
||||||
const buttonHeight = $refreshBtn.outerHeight();
|
|
||||||
|
|
||||||
const minX = 10;
|
|
||||||
const maxX = window.innerWidth - buttonWidth - 10;
|
|
||||||
const minY = 10;
|
|
||||||
const maxY = window.innerHeight - buttonHeight - 10;
|
|
||||||
|
|
||||||
newX = Math.max(minX, Math.min(maxX, newX));
|
|
||||||
newY = Math.max(minY, Math.min(maxY, newY));
|
|
||||||
|
|
||||||
pendingX = newX;
|
|
||||||
pendingY = newY;
|
|
||||||
if (!rafId) {
|
|
||||||
rafId = requestAnimationFrame(updatePosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on('mouseup', function(e) {
|
|
||||||
if (mouseDown && isDragging) {
|
|
||||||
const offset = $refreshBtn.offset();
|
|
||||||
const newPosition = {
|
|
||||||
left: offset.left + 'px',
|
|
||||||
top: offset.top + 'px'
|
|
||||||
};
|
|
||||||
|
|
||||||
extensionSettings.mobileRefreshPosition = newPosition;
|
|
||||||
saveSettings();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
$refreshBtn.removeClass('dragging');
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
$refreshBtn.data('just-dragged', true);
|
|
||||||
setTimeout(() => {
|
|
||||||
$refreshBtn.data('just-dragged', false);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
mouseDown = false;
|
|
||||||
isDragging = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up drag functionality for the debug toggle FAB button
|
|
||||||
* Same pattern as refresh button drag
|
|
||||||
*/
|
|
||||||
export function setupDebugButtonDrag() {
|
|
||||||
const $debugBtn = $('#rpg-debug-toggle');
|
|
||||||
|
|
||||||
if ($debugBtn.length === 0) {
|
|
||||||
console.warn('[RPG Mobile] Debug button not found in DOM');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('[RPG Mobile] setupDebugButtonDrag called');
|
|
||||||
|
|
||||||
// Load and apply saved position
|
|
||||||
if (extensionSettings.debugFabPosition) {
|
|
||||||
const pos = extensionSettings.debugFabPosition;
|
|
||||||
// console.log('[RPG Mobile] Loading saved debug button position:', pos);
|
|
||||||
|
|
||||||
// Apply saved position
|
|
||||||
if (pos.top) $debugBtn.css('top', pos.top);
|
|
||||||
if (pos.right) $debugBtn.css('right', pos.right);
|
|
||||||
if (pos.bottom) $debugBtn.css('bottom', pos.bottom);
|
|
||||||
if (pos.left) $debugBtn.css('left', pos.left);
|
|
||||||
|
|
||||||
// Constrain to viewport after position is applied
|
|
||||||
requestAnimationFrame(() => constrainFabToViewport($debugBtn));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Touch/drag state
|
|
||||||
let isDragging = false;
|
|
||||||
let touchStartTime = 0;
|
|
||||||
let touchStartX = 0;
|
|
||||||
let touchStartY = 0;
|
|
||||||
let buttonStartX = 0;
|
|
||||||
let buttonStartY = 0;
|
|
||||||
const LONG_PRESS_DURATION = 200;
|
|
||||||
const MOVE_THRESHOLD = 10;
|
|
||||||
let rafId = null;
|
|
||||||
let pendingX = null;
|
|
||||||
let pendingY = null;
|
|
||||||
|
|
||||||
// Update position using requestAnimationFrame
|
|
||||||
function updatePosition() {
|
|
||||||
if (pendingX !== null && pendingY !== null) {
|
|
||||||
$debugBtn.css({
|
|
||||||
left: pendingX + 'px',
|
|
||||||
top: pendingY + 'px',
|
|
||||||
right: 'auto',
|
|
||||||
bottom: 'auto'
|
|
||||||
});
|
|
||||||
pendingX = null;
|
|
||||||
pendingY = null;
|
|
||||||
}
|
|
||||||
rafId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Touch start
|
|
||||||
$debugBtn.on('touchstart', function(e) {
|
|
||||||
const touch = e.originalEvent.touches[0];
|
|
||||||
touchStartTime = Date.now();
|
|
||||||
touchStartX = touch.clientX;
|
|
||||||
touchStartY = touch.clientY;
|
|
||||||
|
|
||||||
const offset = $debugBtn.offset();
|
|
||||||
buttonStartX = offset.left;
|
|
||||||
buttonStartY = offset.top;
|
|
||||||
|
|
||||||
isDragging = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Touch move
|
|
||||||
$debugBtn.on('touchmove', function(e) {
|
|
||||||
const touch = e.originalEvent.touches[0];
|
|
||||||
const deltaX = touch.clientX - touchStartX;
|
|
||||||
const deltaY = touch.clientY - touchStartY;
|
|
||||||
const timeSinceStart = Date.now() - touchStartTime;
|
|
||||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
||||||
|
|
||||||
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
|
||||||
isDragging = true;
|
|
||||||
$debugBtn.addClass('dragging');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDragging) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
let newX = buttonStartX + deltaX;
|
|
||||||
let newY = buttonStartY + deltaY;
|
|
||||||
|
|
||||||
const buttonWidth = $debugBtn.outerWidth();
|
|
||||||
const buttonHeight = $debugBtn.outerHeight();
|
|
||||||
|
|
||||||
const minX = 10;
|
|
||||||
const maxX = window.innerWidth - buttonWidth - 10;
|
|
||||||
const minY = 10;
|
|
||||||
const maxY = window.innerHeight - buttonHeight - 10;
|
|
||||||
|
|
||||||
newX = Math.max(minX, Math.min(maxX, newX));
|
|
||||||
newY = Math.max(minY, Math.min(maxY, newY));
|
|
||||||
|
|
||||||
pendingX = newX;
|
|
||||||
pendingY = newY;
|
|
||||||
if (!rafId) {
|
|
||||||
rafId = requestAnimationFrame(updatePosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Touch end
|
|
||||||
$debugBtn.on('touchend', function(e) {
|
|
||||||
if (isDragging) {
|
|
||||||
// Save new position
|
|
||||||
const offset = $debugBtn.offset();
|
|
||||||
const newPosition = {
|
|
||||||
left: offset.left + 'px',
|
|
||||||
top: offset.top + 'px'
|
|
||||||
};
|
|
||||||
|
|
||||||
extensionSettings.debugFabPosition = newPosition;
|
|
||||||
saveSettings();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
$debugBtn.removeClass('dragging');
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
// Set flag to prevent click handler from firing
|
|
||||||
$debugBtn.data('just-dragged', true);
|
|
||||||
setTimeout(() => {
|
|
||||||
$debugBtn.data('just-dragged', false);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
isDragging = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mouse support for desktop
|
|
||||||
let mouseDown = false;
|
|
||||||
|
|
||||||
$debugBtn.on('mousedown', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
touchStartTime = Date.now();
|
|
||||||
touchStartX = e.clientX;
|
|
||||||
touchStartY = e.clientY;
|
|
||||||
|
|
||||||
const offset = $debugBtn.offset();
|
|
||||||
buttonStartX = offset.left;
|
|
||||||
buttonStartY = offset.top;
|
|
||||||
|
|
||||||
mouseDown = true;
|
|
||||||
isDragging = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on('mousemove.rpgDebugDrag', function(e) {
|
|
||||||
if (!mouseDown) return;
|
|
||||||
|
|
||||||
const deltaX = e.clientX - touchStartX;
|
|
||||||
const deltaY = e.clientY - touchStartY;
|
|
||||||
const timeSinceStart = Date.now() - touchStartTime;
|
|
||||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
||||||
|
|
||||||
if (!isDragging && (timeSinceStart > LONG_PRESS_DURATION || distance > MOVE_THRESHOLD)) {
|
|
||||||
isDragging = true;
|
|
||||||
$debugBtn.addClass('dragging');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDragging) {
|
|
||||||
let newX = buttonStartX + deltaX;
|
|
||||||
let newY = buttonStartY + deltaY;
|
|
||||||
|
|
||||||
const buttonWidth = $debugBtn.outerWidth();
|
|
||||||
const buttonHeight = $debugBtn.outerHeight();
|
|
||||||
|
|
||||||
const minX = 10;
|
|
||||||
const maxX = window.innerWidth - buttonWidth - 10;
|
|
||||||
const minY = 10;
|
|
||||||
const maxY = window.innerHeight - buttonHeight - 10;
|
|
||||||
|
|
||||||
newX = Math.max(minX, Math.min(maxX, newX));
|
|
||||||
newY = Math.max(minY, Math.min(maxY, newY));
|
|
||||||
|
|
||||||
pendingX = newX;
|
|
||||||
pendingY = newY;
|
|
||||||
if (!rafId) {
|
|
||||||
rafId = requestAnimationFrame(updatePosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(document).on('mouseup.rpgDebugDrag', function(e) {
|
|
||||||
if (mouseDown && isDragging) {
|
|
||||||
const offset = $debugBtn.offset();
|
|
||||||
const newPosition = {
|
|
||||||
left: offset.left + 'px',
|
|
||||||
top: offset.top + 'px'
|
|
||||||
};
|
|
||||||
|
|
||||||
extensionSettings.debugFabPosition = newPosition;
|
|
||||||
saveSettings();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
$debugBtn.removeClass('dragging');
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
$debugBtn.data('just-dragged', true);
|
|
||||||
setTimeout(() => {
|
|
||||||
$debugBtn.data('just-dragged', false);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
mouseDown = false;
|
|
||||||
isDragging = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// FAB WIDGETS - Info display around FAB button
|
// FAB WIDGETS - Info display around FAB button
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -1322,7 +895,7 @@ export function updateFabWidgets() {
|
|||||||
let infoData = null;
|
let infoData = null;
|
||||||
if (infoBox) {
|
if (infoBox) {
|
||||||
try {
|
try {
|
||||||
infoData = typeof infoBox === 'string' ? JSON.parse(infoBox) : infoBox;
|
infoData = typeof infoBox === 'string' ? repairJSON(infoBox) : infoBox;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[RPG FAB Widgets] Failed to parse infoBox:', e);
|
console.warn('[RPG FAB Widgets] Failed to parse infoBox:', e);
|
||||||
}
|
}
|
||||||
@@ -1332,7 +905,7 @@ export function updateFabWidgets() {
|
|||||||
let statsData = null;
|
let statsData = null;
|
||||||
if (userStats) {
|
if (userStats) {
|
||||||
try {
|
try {
|
||||||
statsData = typeof userStats === 'string' ? JSON.parse(userStats) : userStats;
|
statsData = typeof userStats === 'string' ? repairJSON(userStats) : userStats;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[RPG FAB Widgets] Failed to parse userStats:', e);
|
console.warn('[RPG FAB Widgets] Failed to parse userStats:', e);
|
||||||
}
|
}
|
||||||
@@ -1444,7 +1017,7 @@ export function updateFabWidgets() {
|
|||||||
let allStats = [];
|
let allStats = [];
|
||||||
try {
|
try {
|
||||||
const userStatsJson = extensionSettings.userStats;
|
const userStatsJson = extensionSettings.userStats;
|
||||||
const parsedUserStats = typeof userStatsJson === 'string' ? JSON.parse(userStatsJson) : userStatsJson;
|
const parsedUserStats = typeof userStatsJson === 'string' ? repairJSON(userStatsJson) : userStatsJson;
|
||||||
if (parsedUserStats?.stats) {
|
if (parsedUserStats?.stats) {
|
||||||
allStats = parsedUserStats.stats;
|
allStats = parsedUserStats.stats;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ import { renderEquipment } from '../rendering/equipment.js';
|
|||||||
import {
|
import {
|
||||||
rollDice as rollDiceCore,
|
rollDice as rollDiceCore,
|
||||||
clearDiceRoll as clearDiceRollCore,
|
clearDiceRoll as clearDiceRollCore,
|
||||||
updateDiceDisplay as updateDiceDisplayCore,
|
updateDiceDisplay as updateDiceDisplayCore
|
||||||
addDiceQuickReply as addDiceQuickReplyCore
|
|
||||||
} from '../features/dice.js';
|
} from '../features/dice.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
@@ -578,14 +577,6 @@ export function updateDiceDisplay() {
|
|||||||
updateDiceDisplayCore();
|
updateDiceDisplayCore();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the Roll Dice quick reply button.
|
|
||||||
* Backwards compatible wrapper for dice module.
|
|
||||||
*/
|
|
||||||
export function addDiceQuickReply() {
|
|
||||||
addDiceQuickReplyCore();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the SettingsModal instance for external use
|
* Returns the SettingsModal instance for external use
|
||||||
* @returns {SettingsModal} The global SettingsModal instance
|
* @returns {SettingsModal} The global SettingsModal instance
|
||||||
|
|||||||
+659
-173
@@ -4,12 +4,39 @@
|
|||||||
* Extracted from index.js initUI() to reduce main entry point size.
|
* Extracted from index.js initUI() to reduce main entry point size.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extensionSettings, updateExtensionSettings } from '../../core/state.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { saveSettings } from '../../core/persistence.js';
|
import { extensionSettings, $musicPlayerContainer } from '../../core/state.js';
|
||||||
|
import { saveSettings, commitTrackerDataFromPriorMessage } from '../../core/persistence.js';
|
||||||
/* eslint-disable no-unused-vars */
|
import {
|
||||||
/* These functions receive jQuery-wrapped DOM elements from initUI() */
|
applyPanelPosition,
|
||||||
/* eslint-disable no-unused-vars */
|
updateGenerationModeUI,
|
||||||
|
updateSectionVisibility,
|
||||||
|
togglePlotButtons
|
||||||
|
} from './layout.js';
|
||||||
|
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
|
||||||
|
import { renderUserStats } from '../rendering/userStats.js';
|
||||||
|
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||||
|
import { renderInventory } from '../rendering/inventory.js';
|
||||||
|
import { renderEquipment } from '../rendering/equipment.js';
|
||||||
|
import { renderQuests } from '../rendering/quests.js';
|
||||||
|
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||||||
|
import { toggleDynamicWeather } from './weatherEffects.js';
|
||||||
|
import {
|
||||||
|
applyTheme,
|
||||||
|
updateSettingsPopupTheme,
|
||||||
|
toggleCustomColors,
|
||||||
|
applyCustomTheme,
|
||||||
|
updateFeatureTogglesVisibility
|
||||||
|
} from './theme.js';
|
||||||
|
import {
|
||||||
|
onAlternatePresentCharactersVisibilityChanged,
|
||||||
|
onThoughtBasedExpressionsSettingChanged,
|
||||||
|
onHideDefaultExpressionDisplaySettingChanged
|
||||||
|
} from '../integration/thoughtBasedExpressions.js';
|
||||||
|
import { updateStripWidgets } from './desktop.js';
|
||||||
|
import { updateFabWidgets } from './mobile.js';
|
||||||
|
import { updateDiceDisplay, getSettingsModal } from './modals.js';
|
||||||
|
import { updateRPGData, testExternalAPIConnection } from '../generation/apiClient.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind all settings panel event listeners.
|
* Bind all settings panel event listeners.
|
||||||
@@ -29,6 +56,7 @@ export function bindSettingsListeners($) {
|
|||||||
extensionSettings.panelPosition = String($(this).val());
|
extensionSettings.panelPosition = String($(this).val());
|
||||||
saveSettings();
|
saveSettings();
|
||||||
applyPanelPosition();
|
applyPanelPosition();
|
||||||
|
// Recreate thought bubbles to update their position
|
||||||
updateChatThoughts();
|
updateChatThoughts();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,11 +75,11 @@ export function bindSettingsListeners($) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Section visibility toggles
|
// Section visibility toggles
|
||||||
bindSectionToggle($, 'rpg-toggle-user-stats', 'showUserStats', () => {});
|
bindSectionToggle($, 'rpg-toggle-user-stats', 'showUserStats');
|
||||||
bindSectionToggle($, 'rpg-toggle-info-box', 'showInfoBox', () => {});
|
bindSectionToggle($, 'rpg-toggle-info-box', 'showInfoBox');
|
||||||
bindSectionToggle($, 'rpg-toggle-inventory', 'showInventory', () => {});
|
bindSectionToggle($, 'rpg-toggle-inventory', 'showInventory');
|
||||||
bindSectionToggle($, 'rpg-toggle-equipment', 'showEquipment', () => {});
|
bindSectionToggle($, 'rpg-toggle-equipment', 'showEquipment');
|
||||||
bindSectionToggle($, 'rpg-toggle-quests', 'showQuests', () => {});
|
bindSectionToggle($, 'rpg-toggle-quests', 'showQuests');
|
||||||
|
|
||||||
// Thoughts section with render callback
|
// Thoughts section with render callback
|
||||||
$('#rpg-toggle-thoughts').on('change', function() {
|
$('#rpg-toggle-thoughts').on('change', function() {
|
||||||
@@ -160,25 +188,12 @@ export function bindSettingsListeners($) {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dismiss holiday promo
|
|
||||||
$('#rpg-dismiss-promo').on('click', function() {
|
|
||||||
extensionSettings.dismissedHolidayPromo = true;
|
|
||||||
saveSettings();
|
|
||||||
$('#rpg-holiday-promo').fadeOut(300);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Skip injections for guided mode
|
// Skip injections for guided mode
|
||||||
$('#rpg-skip-guided-mode').on('change', function() {
|
$('#rpg-skip-guided-mode').on('change', function() {
|
||||||
extensionSettings.skipInjectionsForGuided = String($(this).val());
|
extensionSettings.skipInjectionsForGuided = String($(this).val());
|
||||||
saveSettings();
|
saveSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save tracker history
|
|
||||||
$('#rpg-save-tracker-history').on('change', function() {
|
|
||||||
extensionSettings.saveTrackerHistory = $(this).prop('checked');
|
|
||||||
saveSettings();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Randomized plot
|
// Randomized plot
|
||||||
$('#rpg-toggle-randomized-plot').on('change', function() {
|
$('#rpg-toggle-randomized-plot').on('change', function() {
|
||||||
extensionSettings.enableRandomizedPlot = $(this).prop('checked');
|
extensionSettings.enableRandomizedPlot = $(this).prop('checked');
|
||||||
@@ -200,7 +215,7 @@ export function bindSettingsListeners($) {
|
|||||||
}
|
}
|
||||||
extensionSettings.encounterSettings.enabled = $(this).prop('checked');
|
extensionSettings.encounterSettings.enabled = $(this).prop('checked');
|
||||||
saveSettings();
|
saveSettings();
|
||||||
togglePlotButtons();
|
togglePlotButtons(); // This also controls encounter button visibility
|
||||||
});
|
});
|
||||||
|
|
||||||
// Encounter history depth
|
// Encounter history depth
|
||||||
@@ -222,171 +237,642 @@ export function bindSettingsListeners($) {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Combat narrative style settings
|
// Feature toggle visibility controls
|
||||||
bindCombatNarrativeSetting($, 'rpg-combat-tense', 'tense');
|
$('#rpg-toggle-show-html-toggle').on('change', function() {
|
||||||
bindCombatNarrativeSetting($, 'rpg-combat-person', 'person');
|
extensionSettings.showHtmlToggle = $(this).prop('checked');
|
||||||
bindCombatNarrativeSetting($, 'rpg-combat-narration', 'narration');
|
|
||||||
bindCombatNarrativeSetting($, 'rpg-combat-pov', 'pov');
|
|
||||||
|
|
||||||
// Summary narrative style settings
|
|
||||||
bindSummaryNarrativeSetting($, 'rpg-summary-tense', 'tense');
|
|
||||||
bindSummaryNarrativeSetting($, 'rpg-summary-person', 'person');
|
|
||||||
bindSummaryNarrativeSetting($, 'rpg-summary-narration', 'narration');
|
|
||||||
bindSummaryNarrativeSetting($, 'rpg-summary-pov', 'pov');
|
|
||||||
|
|
||||||
// Theme selector
|
|
||||||
$('#rpg-theme-select').on('change', function() {
|
|
||||||
extensionSettings.theme = String($(this).val());
|
|
||||||
saveSettings();
|
|
||||||
applyTheme();
|
|
||||||
updateSettingsPopupTheme();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom theme toggle
|
|
||||||
$('#rpg-toggle-custom-theme').on('change', function() {
|
|
||||||
extensionSettings.enableCustomTheme = $(this).prop('checked');
|
|
||||||
saveSettings();
|
|
||||||
toggleCustomColors();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom theme color pickers
|
|
||||||
$('#rpg-custom-bg-color').on('input', function() {
|
|
||||||
extensionSettings.customBgColor = $(this).val();
|
|
||||||
saveSettings();
|
|
||||||
applyCustomTheme();
|
|
||||||
});
|
|
||||||
$('#rpg-custom-accent-color').on('input', function() {
|
|
||||||
extensionSettings.customAccentColor = $(this).val();
|
|
||||||
saveSettings();
|
|
||||||
applyCustomTheme();
|
|
||||||
});
|
|
||||||
$('#rpg-custom-text-color').on('input', function() {
|
|
||||||
extensionSettings.customTextColor = $(this).val();
|
|
||||||
saveSettings();
|
|
||||||
applyCustomTheme();
|
|
||||||
});
|
|
||||||
$('#rpg-custom-highlight-color').on('input', function() {
|
|
||||||
extensionSettings.customHighlightColor = $(this).val();
|
|
||||||
saveSettings();
|
|
||||||
applyCustomTheme();
|
|
||||||
});
|
|
||||||
$('#rpg-custom-border-color').on('input', function() {
|
|
||||||
extensionSettings.customBorderColor = $(this).val();
|
|
||||||
saveSettings();
|
|
||||||
applyCustomTheme();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom reset button
|
|
||||||
$('#rpg-reset-custom-theme').on('click', function() {
|
|
||||||
extensionSettings.customBgColor = '';
|
|
||||||
extensionSettings.customAccentColor = '';
|
|
||||||
extensionSettings.customTextColor = '';
|
|
||||||
extensionSettings.customHighlightColor = '';
|
|
||||||
extensionSettings.customBorderColor = '';
|
|
||||||
saveSettings();
|
|
||||||
applyTheme();
|
|
||||||
updateSettingsPopupTheme();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animations toggle
|
|
||||||
$('#rpg-toggle-animations').on('change', function() {
|
|
||||||
extensionSettings.enableAnimations = $(this).prop('checked');
|
|
||||||
saveSettings();
|
|
||||||
toggleAnimations();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Panel width
|
|
||||||
$('#rpg-panel-width').on('change', function() {
|
|
||||||
extensionSettings.panelWidth = $(this).val();
|
|
||||||
saveSettings();
|
|
||||||
updatePanelVisibility();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Feature toggles visibility
|
|
||||||
$('#rpg-toggle-feature-toggles').on('change', function() {
|
|
||||||
extensionSettings.showFeatureToggles = $(this).prop('checked');
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
updateFeatureTogglesVisibility();
|
updateFeatureTogglesVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Weather sub-options visibility toggle
|
$('#rpg-toggle-show-dialogue-coloring-toggle').on('change', function() {
|
||||||
$('#rpg-toggle-dynamic-weather-suboptions').on('change', function() {
|
extensionSettings.showDialogueColoringToggle = $(this).prop('checked');
|
||||||
extensionSettings.showDynamicWeatherToggle = $(this).prop('checked');
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-deception-toggle').on('change', function() {
|
||||||
|
extensionSettings.showDeceptionToggle = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-omniscience-toggle').on('change', function() {
|
||||||
|
extensionSettings.showOmniscienceToggle = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-cyoa-toggle').on('change', function() {
|
||||||
|
extensionSettings.showCYOAToggle = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-spotify-toggle').on('change', function() {
|
||||||
|
extensionSettings.showSpotifyToggle = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-dynamic-weather-toggle').on('change', function() {
|
||||||
|
extensionSettings.showDynamicWeatherToggle = $(this).prop('checked');
|
||||||
|
// Also disable the feature when hiding the toggle
|
||||||
|
if (!extensionSettings.showDynamicWeatherToggle) {
|
||||||
|
extensionSettings.enableDynamicWeather = false;
|
||||||
|
$('#rpg-toggle-dynamic-weather').prop('checked', false);
|
||||||
|
toggleDynamicWeather(false);
|
||||||
|
}
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
updateWeatherSubOptionsVisibility();
|
updateWeatherSubOptionsVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Strip widgets
|
// Weather sub-options (background and foreground) - radio buttons
|
||||||
bindStripWidgetToggle($, 'rpg-strip-widget-weather', 'weatherIcon');
|
$('#rpg-toggle-weather-background').on('change', function() {
|
||||||
bindStripWidgetToggle($, 'rpg-strip-widget-clock', 'clock');
|
if ($(this).prop('checked')) {
|
||||||
bindStripWidgetToggle($, 'rpg-strip-widget-date', 'date');
|
extensionSettings.weatherBackground = true;
|
||||||
bindStripWidgetToggle($, 'rpg-strip-widget-location', 'location');
|
extensionSettings.weatherForeground = false;
|
||||||
bindStripWidgetToggle($, 'rpg-strip-widget-stats', 'stats');
|
saveSettings();
|
||||||
bindStripWidgetToggle($, 'rpg-strip-widget-attributes', 'attributes');
|
// Re-apply weather effect
|
||||||
|
if (extensionSettings.enableDynamicWeather) {
|
||||||
// Mobile panel position
|
toggleDynamicWeather(false);
|
||||||
$('#rpg-mobile-position-select').on('change', function() {
|
toggleDynamicWeather(true);
|
||||||
extensionSettings.mobilePanelPosition = String($(this).val());
|
}
|
||||||
saveSettings();
|
}
|
||||||
updateMobilePanelPosition();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-weather-foreground').on('change', function() {
|
||||||
|
if ($(this).prop('checked')) {
|
||||||
|
extensionSettings.weatherBackground = false;
|
||||||
|
extensionSettings.weatherForeground = true;
|
||||||
|
saveSettings();
|
||||||
|
// Re-apply weather effect
|
||||||
|
if (extensionSettings.enableDynamicWeather) {
|
||||||
|
toggleDynamicWeather(false);
|
||||||
|
toggleDynamicWeather(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-narrator-mode').on('change', function() {
|
||||||
|
extensionSettings.showNarratorMode = $(this).prop('checked');
|
||||||
|
// Also disable the feature when hiding the toggle
|
||||||
|
if (!extensionSettings.showNarratorMode) {
|
||||||
|
extensionSettings.narratorMode = false;
|
||||||
|
$('#rpg-toggle-narrator').prop('checked', false);
|
||||||
|
}
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-auto-avatars').on('change', function() {
|
||||||
|
extensionSettings.showAutoAvatars = $(this).prop('checked');
|
||||||
|
// Also disable the feature when hiding the toggle
|
||||||
|
if (!extensionSettings.showAutoAvatars) {
|
||||||
|
extensionSettings.autoGenerateAvatars = false;
|
||||||
|
$('#rpg-toggle-auto-avatars-panel').prop('checked', false);
|
||||||
|
}
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto avatar generation panel toggle
|
||||||
|
$('#rpg-toggle-auto-avatars-panel').on('change', function() {
|
||||||
|
extensionSettings.autoGenerateAvatars = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
|
||||||
|
// Re-render thoughts to update tooltips (regenerate vs delete)
|
||||||
|
renderThoughts();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dice display toggle
|
||||||
|
$('#rpg-toggle-dice-display').on('change', function() {
|
||||||
|
extensionSettings.showDiceDisplay = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateDiceDisplay();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mobile FAB Widget toggles - simplified, no position saving (auto-positioned)
|
||||||
|
$('#rpg-toggle-fab-widgets-enabled').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
extensionSettings.mobileFabWidgets.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
$('#rpg-fab-widget-options').toggle(extensionSettings.mobileFabWidgets.enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-weather-icon').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.weatherIcon) extensionSettings.mobileFabWidgets.weatherIcon = {};
|
||||||
|
extensionSettings.mobileFabWidgets.weatherIcon.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-weather-desc').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.weatherDesc) extensionSettings.mobileFabWidgets.weatherDesc = {};
|
||||||
|
extensionSettings.mobileFabWidgets.weatherDesc.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-clock').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.clock) extensionSettings.mobileFabWidgets.clock = {};
|
||||||
|
extensionSettings.mobileFabWidgets.clock.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-date').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.date) extensionSettings.mobileFabWidgets.date = {};
|
||||||
|
extensionSettings.mobileFabWidgets.date.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-location').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.location) extensionSettings.mobileFabWidgets.location = {};
|
||||||
|
extensionSettings.mobileFabWidgets.location.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-stats').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.stats) extensionSettings.mobileFabWidgets.stats = {};
|
||||||
|
extensionSettings.mobileFabWidgets.stats.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-attributes').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.attributes) extensionSettings.mobileFabWidgets.attributes = {};
|
||||||
|
extensionSettings.mobileFabWidgets.attributes.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Desktop Strip Widget toggles
|
||||||
|
$('#rpg-toggle-strip-widgets-enabled').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
extensionSettings.desktopStripWidgets.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
$('#rpg-strip-widget-options').toggle(extensionSettings.desktopStripWidgets.enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-weather-icon').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.weatherIcon) extensionSettings.desktopStripWidgets.weatherIcon = {};
|
||||||
|
extensionSettings.desktopStripWidgets.weatherIcon.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-clock').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.clock) extensionSettings.desktopStripWidgets.clock = {};
|
||||||
|
extensionSettings.desktopStripWidgets.clock.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-date').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.date) extensionSettings.desktopStripWidgets.date = {};
|
||||||
|
extensionSettings.desktopStripWidgets.date.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-location').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.location) extensionSettings.desktopStripWidgets.location = {};
|
||||||
|
extensionSettings.desktopStripWidgets.location.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-stats').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.stats) extensionSettings.desktopStripWidgets.stats = {};
|
||||||
|
extensionSettings.desktopStripWidgets.stats.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-attributes').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.attributes) extensionSettings.desktopStripWidgets.attributes = {};
|
||||||
|
extensionSettings.desktopStripWidgets.attributes.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual update button
|
||||||
|
$('#rpg-manual-update').on('click', async function() {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentChat = getContext().chat;
|
||||||
|
let lastAssistantIndex = -1;
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||||||
|
lastAssistantIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastAssistantIndex !== -1) {
|
||||||
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
|
}
|
||||||
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Strip widget refresh button - same functionality as main refresh button
|
||||||
|
$('#rpg-strip-refresh').on('click', async function() {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentChat = getContext().chat;
|
||||||
|
let lastAssistantIndex = -1;
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||||||
|
lastAssistantIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastAssistantIndex !== -1) {
|
||||||
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
|
}
|
||||||
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stat bar colors
|
||||||
|
$('#rpg-stat-bar-color-low').on('change', function() {
|
||||||
|
extensionSettings.statBarColorLow = String($(this).val());
|
||||||
|
saveSettings();
|
||||||
|
renderUserStats(); // Re-render with new colors
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-stat-bar-color-low-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.statBarColorLowOpacity = opacity;
|
||||||
|
$('#rpg-stat-bar-color-low-opacity-value').text(opacity + '%');
|
||||||
|
renderUserStats();
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-stat-bar-color-high').on('change', function() {
|
||||||
|
extensionSettings.statBarColorHigh = String($(this).val());
|
||||||
|
saveSettings();
|
||||||
|
renderUserStats(); // Re-render with new colors
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-stat-bar-color-high-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.statBarColorHighOpacity = opacity;
|
||||||
|
$('#rpg-stat-bar-color-high-opacity-value').text(opacity + '%');
|
||||||
|
renderUserStats();
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme selection
|
||||||
|
$('#rpg-theme-select').on('change', function() {
|
||||||
|
extensionSettings.theme = String($(this).val());
|
||||||
|
saveSettings();
|
||||||
|
applyTheme();
|
||||||
|
toggleCustomColors();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal()); // Update popup theme instantly
|
||||||
|
updateChatThoughts(); // Recreate thought bubbles with new theme
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom color pickers
|
||||||
|
$('#rpg-custom-bg').on('change', function() {
|
||||||
|
extensionSettings.customColors.bg = String($(this).val());
|
||||||
|
saveSettings();
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-bg-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.customColors.bgOpacity = opacity;
|
||||||
|
$('#rpg-custom-bg-opacity-value').text(opacity + '%');
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-accent').on('change', function() {
|
||||||
|
extensionSettings.customColors.accent = String($(this).val());
|
||||||
|
saveSettings();
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-accent-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.customColors.accentOpacity = opacity;
|
||||||
|
$('#rpg-custom-accent-opacity-value').text(opacity + '%');
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-text').on('change', function() {
|
||||||
|
extensionSettings.customColors.text = String($(this).val());
|
||||||
|
saveSettings();
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-text-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.customColors.textOpacity = opacity;
|
||||||
|
$('#rpg-custom-text-opacity-value').text(opacity + '%');
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-highlight').on('change', function() {
|
||||||
|
extensionSettings.customColors.highlight = String($(this).val());
|
||||||
|
saveSettings();
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-highlight-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.customColors.highlightOpacity = opacity;
|
||||||
|
$('#rpg-custom-highlight-opacity-value').text(opacity + '%');
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
// External API settings event handlers
|
||||||
|
$('#rpg-external-base-url').on('change', function() {
|
||||||
|
if (!extensionSettings.externalApiSettings) {
|
||||||
|
extensionSettings.externalApiSettings = {
|
||||||
|
baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7
|
||||||
|
};
|
||||||
|
}
|
||||||
|
extensionSettings.externalApiSettings.baseUrl = String($(this).val()).trim();
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-external-api-key').on('change', function() {
|
||||||
|
// Securely store API key in localStorage instead of shared extension settings
|
||||||
|
const apiKey = String($(this).val()).trim();
|
||||||
|
localStorage.setItem('rpg_companion_external_api_key', apiKey);
|
||||||
|
|
||||||
|
// Ensure the externalApiSettings object exists, but don't store the key in it
|
||||||
|
if (!extensionSettings.externalApiSettings) {
|
||||||
|
extensionSettings.externalApiSettings = {
|
||||||
|
baseUrl: '', model: '', maxTokens: 8192, temperature: 0.7
|
||||||
|
};
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-external-model').on('change', function() {
|
||||||
|
if (!extensionSettings.externalApiSettings) {
|
||||||
|
extensionSettings.externalApiSettings = {
|
||||||
|
baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7
|
||||||
|
};
|
||||||
|
}
|
||||||
|
extensionSettings.externalApiSettings.model = String($(this).val()).trim();
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-external-max-tokens').on('change', function() {
|
||||||
|
if (!extensionSettings.externalApiSettings) {
|
||||||
|
extensionSettings.externalApiSettings = {
|
||||||
|
baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7
|
||||||
|
};
|
||||||
|
}
|
||||||
|
extensionSettings.externalApiSettings.maxTokens = parseInt(String($(this).val()));
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-external-temperature').on('change', function() {
|
||||||
|
if (!extensionSettings.externalApiSettings) {
|
||||||
|
extensionSettings.externalApiSettings = {
|
||||||
|
baseUrl: '', apiKey: '', model: '', maxTokens: 8192, temperature: 0.7
|
||||||
|
};
|
||||||
|
}
|
||||||
|
extensionSettings.externalApiSettings.temperature = parseFloat(String($(this).val()));
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-api-key-visibility').on('click', function() {
|
||||||
|
const $input = $('#rpg-external-api-key');
|
||||||
|
const type = $input.attr('type') === 'password' ? 'text' : 'password';
|
||||||
|
$input.attr('type', type);
|
||||||
|
$(this).find('i').toggleClass('fa-eye fa-eye-slash');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-test-external-api').on('click', async function() {
|
||||||
|
const $result = $('#rpg-external-api-test-result');
|
||||||
|
const $btn = $(this);
|
||||||
|
const originalText = $btn.html();
|
||||||
|
|
||||||
|
$btn.html('<i class="fa-solid fa-spinner fa-spin"></i> Testing...').prop('disabled', true);
|
||||||
|
$result.hide().removeClass('rpg-success-message rpg-error-message');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await testExternalAPIConnection();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
$result.addClass('rpg-success-message')
|
||||||
|
.html(`<i class="fa-solid fa-check-circle"></i> ${result.message}`)
|
||||||
|
.slideDown();
|
||||||
|
toastr.success(result.message);
|
||||||
|
} else {
|
||||||
|
$result.addClass('rpg-error-message')
|
||||||
|
.html(`<i class="fa-solid fa-exclamation-circle"></i> ${result.message}`)
|
||||||
|
.slideDown();
|
||||||
|
toastr.error(result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
$result.addClass('rpg-error-message')
|
||||||
|
.html(`<i class="fa-solid fa-exclamation-circle"></i> Error: ${error.message}`)
|
||||||
|
.slideDown();
|
||||||
|
} finally {
|
||||||
|
$btn.html(originalText).prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize all settings modal inputs with the currently saved settings.
|
||||||
|
* Called once during UI initialization after the template is in the DOM.
|
||||||
|
*/
|
||||||
|
export function initializeSettingsUIState() {
|
||||||
|
// Initialize UI state (enable/disable is in Extensions tab)
|
||||||
|
$('#rpg-toggle-auto-update').prop('checked', extensionSettings.autoUpdate);
|
||||||
|
$('#rpg-position-select').val(extensionSettings.panelPosition);
|
||||||
|
$('#rpg-update-depth').val(extensionSettings.updateDepth);
|
||||||
|
$('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats);
|
||||||
|
$('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox);
|
||||||
|
$('#rpg-toggle-thoughts').prop('checked', extensionSettings.showCharacterThoughts);
|
||||||
|
$('#rpg-toggle-alt-present-characters').prop('checked', extensionSettings.showAlternatePresentCharactersPanel ?? false);
|
||||||
|
$('#rpg-toggle-thought-based-expressions').prop('checked', extensionSettings.enableThoughtBasedExpressions === true);
|
||||||
|
$('#rpg-toggle-hide-default-expressions').prop('checked', extensionSettings.hideDefaultExpressionDisplay === true);
|
||||||
|
$('#rpg-toggle-inventory').prop('checked', extensionSettings.showInventory);
|
||||||
|
$('#rpg-toggle-equipment').prop('checked', extensionSettings.showEquipment);
|
||||||
|
$('#rpg-toggle-quests').prop('checked', extensionSettings.showQuests);
|
||||||
|
$('#rpg-toggle-lock-icons').prop('checked', extensionSettings.showLockIcons ?? true);
|
||||||
|
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
|
||||||
|
$('#rpg-toggle-inline-thoughts').prop('checked', (extensionSettings.thoughtsInChatStyle || 'corner') === 'inline');
|
||||||
|
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
|
||||||
|
$('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring);
|
||||||
|
$('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false);
|
||||||
|
$('#rpg-toggle-omniscience').prop('checked', extensionSettings.enableOmniscienceFilter ?? false);
|
||||||
|
$('#rpg-toggle-cyoa').prop('checked', extensionSettings.enableCYOA ?? false);
|
||||||
|
$('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic);
|
||||||
|
|
||||||
|
$('#rpg-toggle-dynamic-weather').prop('checked', extensionSettings.enableDynamicWeather);
|
||||||
|
$('#rpg-toggle-narrator').prop('checked', extensionSettings.narratorMode);
|
||||||
|
|
||||||
|
// Feature toggle visibility settings
|
||||||
|
$('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true);
|
||||||
|
$('#rpg-toggle-show-dialogue-coloring-toggle').prop('checked', extensionSettings.showDialogueColoringToggle ?? true);
|
||||||
|
$('#rpg-toggle-show-deception-toggle').prop('checked', extensionSettings.showDeceptionToggle ?? true);
|
||||||
|
$('#rpg-toggle-show-omniscience-toggle').prop('checked', extensionSettings.showOmniscienceToggle ?? true);
|
||||||
|
$('#rpg-toggle-show-cyoa-toggle').prop('checked', extensionSettings.showCYOAToggle ?? true);
|
||||||
|
$('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true);
|
||||||
|
$('#rpg-toggle-show-dynamic-weather-toggle').prop('checked', extensionSettings.showDynamicWeatherToggle ?? true);
|
||||||
|
$('#rpg-toggle-weather-background').prop('checked', extensionSettings.weatherBackground ?? true);
|
||||||
|
$('#rpg-toggle-weather-foreground').prop('checked', extensionSettings.weatherForeground ?? false);
|
||||||
|
$('#rpg-toggle-show-narrator-mode').prop('checked', extensionSettings.showNarratorMode ?? true);
|
||||||
|
$('#rpg-toggle-show-auto-avatars').prop('checked', extensionSettings.showAutoAvatars ?? true);
|
||||||
|
|
||||||
|
$('#rpg-toggle-randomized-plot').prop('checked', extensionSettings.enableRandomizedPlot ?? true);
|
||||||
|
$('#rpg-toggle-natural-plot').prop('checked', extensionSettings.enableNaturalPlot ?? true);
|
||||||
|
$('#rpg-toggle-encounters').prop('checked', extensionSettings.encounterSettings?.enabled ?? true);
|
||||||
|
$('#rpg-encounter-history-depth').val(extensionSettings.encounterSettings?.historyDepth ?? 8);
|
||||||
|
$('#rpg-toggle-autosave-logs').prop('checked', extensionSettings.encounterSettings?.autoSaveLogs ?? true);
|
||||||
|
|
||||||
|
// Initialize avatar options (panel toggle)
|
||||||
|
$('#rpg-toggle-auto-avatars-panel').prop('checked', extensionSettings.autoGenerateAvatars || false);
|
||||||
|
|
||||||
|
$('#rpg-toggle-dice-display').prop('checked', extensionSettings.showDiceDisplay);
|
||||||
|
|
||||||
|
// Initialize Mobile FAB Widget checkboxes
|
||||||
|
const fabWidgets = extensionSettings.mobileFabWidgets || {};
|
||||||
|
$('#rpg-toggle-fab-widgets-enabled').prop('checked', fabWidgets.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-weather-icon').prop('checked', fabWidgets.weatherIcon?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-weather-desc').prop('checked', fabWidgets.weatherDesc?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-clock').prop('checked', fabWidgets.clock?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-date').prop('checked', fabWidgets.date?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-location').prop('checked', fabWidgets.location?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-stats').prop('checked', fabWidgets.stats?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-attributes').prop('checked', fabWidgets.attributes?.enabled || false);
|
||||||
|
// Toggle visibility of widget options based on master toggle
|
||||||
|
$('#rpg-fab-widget-options').toggle(fabWidgets.enabled || false);
|
||||||
|
|
||||||
|
// Initialize Desktop Strip Widget checkboxes
|
||||||
|
const stripWidgets = extensionSettings.desktopStripWidgets || {};
|
||||||
|
$('#rpg-toggle-strip-widgets-enabled').prop('checked', stripWidgets.enabled || false);
|
||||||
|
$('#rpg-toggle-strip-weather-icon').prop('checked', stripWidgets.weatherIcon?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-clock').prop('checked', stripWidgets.clock?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-date').prop('checked', stripWidgets.date?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-location').prop('checked', stripWidgets.location?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-stats').prop('checked', stripWidgets.stats?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-attributes').prop('checked', stripWidgets.attributes?.enabled ?? true);
|
||||||
|
// Toggle visibility of strip widget options based on master toggle
|
||||||
|
$('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false);
|
||||||
|
|
||||||
|
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
|
||||||
|
$('#rpg-stat-bar-color-low-opacity').val(extensionSettings.statBarColorLowOpacity ?? 100);
|
||||||
|
$('#rpg-stat-bar-color-low-opacity-value').text((extensionSettings.statBarColorLowOpacity ?? 100) + '%');
|
||||||
|
|
||||||
|
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
|
||||||
|
$('#rpg-stat-bar-color-high-opacity').val(extensionSettings.statBarColorHighOpacity ?? 100);
|
||||||
|
$('#rpg-stat-bar-color-high-opacity-value').text((extensionSettings.statBarColorHighOpacity ?? 100) + '%');
|
||||||
|
|
||||||
|
$('#rpg-theme-select').val(extensionSettings.theme);
|
||||||
|
$('#rpg-custom-bg').val(extensionSettings.customColors.bg);
|
||||||
|
$('#rpg-custom-bg-opacity').val(extensionSettings.customColors.bgOpacity ?? 100);
|
||||||
|
$('#rpg-custom-bg-opacity-value').text((extensionSettings.customColors.bgOpacity ?? 100) + '%');
|
||||||
|
|
||||||
|
$('#rpg-custom-accent').val(extensionSettings.customColors.accent);
|
||||||
|
$('#rpg-custom-accent-opacity').val(extensionSettings.customColors.accentOpacity ?? 100);
|
||||||
|
$('#rpg-custom-accent-opacity-value').text((extensionSettings.customColors.accentOpacity ?? 100) + '%');
|
||||||
|
|
||||||
|
$('#rpg-custom-text').val(extensionSettings.customColors.text);
|
||||||
|
$('#rpg-custom-text-opacity').val(extensionSettings.customColors.textOpacity ?? 100);
|
||||||
|
$('#rpg-custom-text-opacity-value').text((extensionSettings.customColors.textOpacity ?? 100) + '%');
|
||||||
|
|
||||||
|
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
|
||||||
|
$('#rpg-custom-highlight-opacity').val(extensionSettings.customColors.highlightOpacity ?? 100);
|
||||||
|
$('#rpg-custom-highlight-opacity-value').text((extensionSettings.customColors.highlightOpacity ?? 100) + '%');
|
||||||
|
|
||||||
|
// Initialize External API settings values
|
||||||
|
if (extensionSettings.externalApiSettings) {
|
||||||
|
$('#rpg-external-base-url').val(extensionSettings.externalApiSettings.baseUrl || '');
|
||||||
|
|
||||||
|
// Load API Key from secure localStorage
|
||||||
|
const storedApiKey = localStorage.getItem('rpg_companion_external_api_key') || '';
|
||||||
|
$('#rpg-external-api-key').val(storedApiKey);
|
||||||
|
|
||||||
|
$('#rpg-external-model').val(extensionSettings.externalApiSettings.model || '');
|
||||||
|
$('#rpg-external-max-tokens').val(extensionSettings.externalApiSettings.maxTokens || 8192);
|
||||||
|
$('#rpg-external-temperature').val(extensionSettings.externalApiSettings.temperature ?? 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#rpg-generation-mode').val(extensionSettings.generationMode);
|
||||||
|
$('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided);
|
||||||
|
|
||||||
|
// Weather sub-options visibility follows the dynamic weather toggle visibility setting
|
||||||
|
updateWeatherSubOptionsVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper: Bind a simple section visibility toggle
|
* Helper: Bind a simple section visibility toggle
|
||||||
*/
|
*/
|
||||||
function bindSectionToggle($, elementId, settingKey, callback) {
|
function bindSectionToggle($, elementId, settingKey) {
|
||||||
$(`#${elementId}`).on('change', function() {
|
$(`#${elementId}`).on('change', function() {
|
||||||
extensionSettings[settingKey] = $(this).prop('checked');
|
extensionSettings[settingKey] = $(this).prop('checked');
|
||||||
saveSettings();
|
saveSettings();
|
||||||
updateSectionVisibility();
|
updateSectionVisibility();
|
||||||
if (callback) callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Bind combat narrative setting
|
|
||||||
*/
|
|
||||||
function bindCombatNarrativeSetting($, elementId, key) {
|
|
||||||
$(`#${elementId}`).on('change', function() {
|
|
||||||
if (!extensionSettings.encounterSettings) {
|
|
||||||
extensionSettings.encounterSettings = {};
|
|
||||||
}
|
|
||||||
if (!extensionSettings.encounterSettings.combatNarrative) {
|
|
||||||
extensionSettings.encounterSettings.combatNarrative = {};
|
|
||||||
}
|
|
||||||
extensionSettings.encounterSettings.combatNarrative[key] = $(this).val();
|
|
||||||
saveSettings();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Bind summary narrative setting
|
|
||||||
*/
|
|
||||||
function bindSummaryNarrativeSetting($, elementId, key) {
|
|
||||||
$(`#${elementId}`).on('change', function() {
|
|
||||||
if (!extensionSettings.encounterSettings) {
|
|
||||||
extensionSettings.encounterSettings = {};
|
|
||||||
}
|
|
||||||
if (!extensionSettings.encounterSettings.summaryNarrative) {
|
|
||||||
extensionSettings.encounterSettings.summaryNarrative = {};
|
|
||||||
}
|
|
||||||
extensionSettings.encounterSettings.summaryNarrative[key] = $(this).val();
|
|
||||||
saveSettings();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Bind strip widget toggle
|
|
||||||
*/
|
|
||||||
function bindStripWidgetToggle($, elementId, widgetKey) {
|
|
||||||
$(`#${elementId}`).on('change', function() {
|
|
||||||
if (!extensionSettings.stripWidgets) {
|
|
||||||
extensionSettings.stripWidgets = { weatherIcon: true, clock: true, date: true, location: true, stats: true, attributes: true };
|
|
||||||
}
|
|
||||||
extensionSettings.stripWidgets[widgetKey] = $(this).prop('checked');
|
|
||||||
saveSettings();
|
|
||||||
updateStripWidgets();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,4 +888,4 @@ export function updateWeatherSubOptionsVisibility() {
|
|||||||
} else {
|
} else {
|
||||||
$weatherSubOptions.hide();
|
$weatherSubOptions.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1671,7 +1671,7 @@ function setupHistoryPersistenceListeners() {
|
|||||||
messageCount: 5,
|
messageCount: 5,
|
||||||
injectionPosition: 'assistant_message_end',
|
injectionPosition: 'assistant_message_end',
|
||||||
contextPreamble: '',
|
contextPreamble: '',
|
||||||
externalApiOnly: false
|
sendAllEnabledOnRefresh: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ function getCurrentWeather() {
|
|||||||
|
|
||||||
// Try to parse as JSON first (new format)
|
// Try to parse as JSON first (new format)
|
||||||
try {
|
try {
|
||||||
const parsed = typeof infoBoxData === 'string' ? JSON.parse(infoBoxData) : infoBoxData;
|
const parsed = typeof infoBoxData === 'string' ? repairJSON(infoBoxData) : infoBoxData;
|
||||||
if (parsed && parsed.weather) {
|
if (parsed && parsed.weather) {
|
||||||
// Return the forecast text from the weather object
|
// Return the forecast text from the weather object
|
||||||
return parsed.weather.forecast || parsed.weather.emoji || null;
|
return parsed.weather.forecast || parsed.weather.emoji || null;
|
||||||
|
|||||||
+41
-15
@@ -3,6 +3,31 @@
|
|||||||
* Handles parsing and repairing malformed JSON from AI responses
|
* Handles parsing and repairing malformed JSON from AI responses
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* ===== Pre-compiled regex patterns (module-level constants) ===== */
|
||||||
|
|
||||||
|
// Markdown code fence removal
|
||||||
|
const MARKDOWN_JSON_FENCE_RE = /```json\s*/gi;
|
||||||
|
const MARKDOWN_GENERIC_FENCE_RE = /```\s*/g;
|
||||||
|
|
||||||
|
// Thinking tag removal
|
||||||
|
const THINKING_TAG_RE = /<think>[\s\S]*?<\/think>/gi;
|
||||||
|
const THINKING_TAG_ALT_RE = /<thinking>[\s\S]*?<\/thinking>/gi;
|
||||||
|
|
||||||
|
// JSON repair patterns
|
||||||
|
const TRAILING_COMMA_RE = /,(\s*[}\]])/g;
|
||||||
|
const JS_LINE_COMMENT_RE = /\/\/.*$/gm;
|
||||||
|
const JS_BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
||||||
|
|
||||||
|
// JSON extraction patterns
|
||||||
|
const JSON_OBJECT_RE = /\{[\s\S]*\}/;
|
||||||
|
const JSON_ARRAY_RE = /\[[\s\S]*\]/;
|
||||||
|
|
||||||
|
// extractJSONFromText patterns
|
||||||
|
const FENCE_JSON_RE = /```json\s*([\s\S]*?)```/i;
|
||||||
|
const FENCE_GENERIC_RE = /```\s*([\s\S]*?)```/;
|
||||||
|
const STANDALONE_OBJECT_RE = /\{[\s\S]*\}/;
|
||||||
|
const STANDALONE_ARRAY_RE = /\[[\s\S]*\]/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repairs malformed JSON from AI responses
|
* Repairs malformed JSON from AI responses
|
||||||
* Handles common AI mistakes like trailing commas, missing commas, wrong quotes, etc.
|
* Handles common AI mistakes like trailing commas, missing commas, wrong quotes, etc.
|
||||||
@@ -23,17 +48,17 @@ export function repairJSON(jsonString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove markdown code fences
|
// Remove markdown code fences
|
||||||
cleaned = cleaned.replace(/```json\s*/gi, '');
|
cleaned = cleaned.replace(MARKDOWN_JSON_FENCE_RE, '');
|
||||||
cleaned = cleaned.replace(/```\s*/g, '');
|
cleaned = cleaned.replace(MARKDOWN_GENERIC_FENCE_RE, '');
|
||||||
|
|
||||||
// Remove thinking tags (model's internal reasoning)
|
// Remove thinking tags (model's internal reasoning)
|
||||||
cleaned = cleaned.replace(/<think>[\s\S]*?<\/think>/gi, '');
|
cleaned = cleaned.replace(THINKING_TAG_RE, '');
|
||||||
cleaned = cleaned.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
|
cleaned = cleaned.replace(THINKING_TAG_ALT_RE, '');
|
||||||
|
|
||||||
// Fix common JSON errors:
|
// Fix common JSON errors:
|
||||||
|
|
||||||
// 1. Trailing commas before closing brackets
|
// 1. Trailing commas before closing brackets
|
||||||
cleaned = cleaned.replace(/,(\s*[}\]])/g, '$1');
|
cleaned = cleaned.replace(TRAILING_COMMA_RE, '$1');
|
||||||
|
|
||||||
// 2. Missing commas between properties - DISABLED because it corrupts valid JSON
|
// 2. Missing commas between properties - DISABLED because it corrupts valid JSON
|
||||||
// Modern AI models send properly formatted JSON, so this aggressive repair is not needed
|
// Modern AI models send properly formatted JSON, so this aggressive repair is not needed
|
||||||
@@ -49,32 +74,33 @@ export function repairJSON(jsonString) {
|
|||||||
// cleaned = cleaned.replace(/(\{|,)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
|
// cleaned = cleaned.replace(/(\{|,)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
|
||||||
|
|
||||||
// 5. Remove JavaScript comments
|
// 5. Remove JavaScript comments
|
||||||
cleaned = cleaned.replace(/\/\/.*$/gm, '');
|
cleaned = cleaned.replace(JS_LINE_COMMENT_RE, '');
|
||||||
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '');
|
cleaned = cleaned.replace(JS_BLOCK_COMMENT_RE, '');
|
||||||
|
|
||||||
// Attempt 1: Standard JSON.parse
|
// Attempt 1: Standard JSON.parse
|
||||||
try {
|
try {
|
||||||
return JSON.parse(cleaned);
|
return JSON.parse(cleaned);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.debug('[RPG JSON Repair] Attempt 1 (JSON.parse) failed:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt 2: Extract JSON object between first { and last }
|
// Attempt 2: Extract JSON object between first { and last }
|
||||||
const objectMatch = cleaned.match(/\{[\s\S]*\}/);
|
const objectMatch = cleaned.match(JSON_OBJECT_RE);
|
||||||
if (objectMatch) {
|
if (objectMatch) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(objectMatch[0]);
|
return JSON.parse(objectMatch[0]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail, try next method
|
console.debug('[RPG JSON Repair] Attempt 2 (object extraction) failed:', e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt 3: Try to extract JSON array between first [ and last ]
|
// Attempt 3: Try to extract JSON array between first [ and last ]
|
||||||
const arrayMatch = cleaned.match(/\[[\s\S]*\]/);
|
const arrayMatch = cleaned.match(JSON_ARRAY_RE);
|
||||||
if (arrayMatch) {
|
if (arrayMatch) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(arrayMatch[0]);
|
return JSON.parse(arrayMatch[0]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail, try next method
|
console.debug('[RPG JSON Repair] Attempt 3 (array extraction) failed:', e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,14 +175,14 @@ export function extractJSONFromText(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract from ```json code fence
|
// Try to extract from ```json code fence
|
||||||
const fenceMatch = text.match(/```json\s*([\s\S]*?)```/i);
|
const fenceMatch = text.match(FENCE_JSON_RE);
|
||||||
if (fenceMatch && fenceMatch[1]) {
|
if (fenceMatch && fenceMatch[1]) {
|
||||||
const trimmed = fenceMatch[1].trim();
|
const trimmed = fenceMatch[1].trim();
|
||||||
if (trimmed) return trimmed;
|
if (trimmed) return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract from ``` code fence (without json label)
|
// Try to extract from ``` code fence (without json label)
|
||||||
const genericFenceMatch = text.match(/```\s*([\s\S]*?)```/);
|
const genericFenceMatch = text.match(FENCE_GENERIC_RE);
|
||||||
if (genericFenceMatch && genericFenceMatch[1]) {
|
if (genericFenceMatch && genericFenceMatch[1]) {
|
||||||
const content = genericFenceMatch[1].trim();
|
const content = genericFenceMatch[1].trim();
|
||||||
// Check if it looks like JSON (starts with { or [)
|
// Check if it looks like JSON (starts with { or [)
|
||||||
@@ -166,13 +192,13 @@ export function extractJSONFromText(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to find standalone JSON object
|
// Try to find standalone JSON object
|
||||||
const objectMatch = text.match(/\{[\s\S]*\}/);
|
const objectMatch = text.match(STANDALONE_OBJECT_RE);
|
||||||
if (objectMatch && objectMatch[0].trim()) {
|
if (objectMatch && objectMatch[0].trim()) {
|
||||||
return objectMatch[0];
|
return objectMatch[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find standalone JSON array
|
// Try to find standalone JSON array
|
||||||
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
const arrayMatch = text.match(STANDALONE_ARRAY_RE);
|
||||||
if (arrayMatch && arrayMatch[0].trim()) {
|
if (arrayMatch && arrayMatch[0].trim()) {
|
||||||
return arrayMatch[0];
|
return arrayMatch[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* intercepts the raw fetch response as a fallback.
|
* intercepts the raw fetch response as a fallback.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateRaw } from '../../../../../../../script.js';
|
import { generateRaw } from '../../../../../../script.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts text from any API response shape (Anthropic content-block arrays,
|
* Extracts text from any API response shape (Anthropic content-block arrays,
|
||||||
|
|||||||
+75
-49
@@ -23,6 +23,70 @@ const BLOCKED_PROPERTY_NAMES = [
|
|||||||
'__lookupSetter__'
|
'__lookupSetter__'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a generic string validator/sanitizer with configurable rules.
|
||||||
|
* Returns a function that validates and sanitizes input strings.
|
||||||
|
*
|
||||||
|
* @param {Object} options - Validator configuration
|
||||||
|
* @param {string} options.label - Human-readable label for logging (e.g., 'location name', 'item name')
|
||||||
|
* @param {number} options.maxLength - Maximum allowed string length
|
||||||
|
* @param {string[]} [options.blockedNames] - Array of blocked names (defaults to BLOCKED_PROPERTY_NAMES)
|
||||||
|
* @param {string[]} [options.additionalRejects] - Additional values to reject (lowercase comparison, e.g., ['none'])
|
||||||
|
* @param {boolean} [options.checkBlockedNames] - Whether to check against blocked property names (default: true)
|
||||||
|
* @returns {Function} Sanitizer function: (string) => string|null
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const sanitizeLocation = createStringValidator({
|
||||||
|
* label: 'location name',
|
||||||
|
* maxLength: 200,
|
||||||
|
* checkBlockedNames: true
|
||||||
|
* });
|
||||||
|
* const sanitizeItem = createStringValidator({
|
||||||
|
* label: 'item name',
|
||||||
|
* maxLength: 500,
|
||||||
|
* additionalRejects: ['none'],
|
||||||
|
* checkBlockedNames: false
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function createStringValidator(options) {
|
||||||
|
const { label, maxLength, blockedNames = BLOCKED_PROPERTY_NAMES, additionalRejects = [], checkBlockedNames = true } = options;
|
||||||
|
|
||||||
|
return function sanitize(name) {
|
||||||
|
if (!name || typeof name !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = name.trim();
|
||||||
|
|
||||||
|
// Empty check
|
||||||
|
if (trimmed === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional rejects (e.g., 'none' for items)
|
||||||
|
if (additionalRejects.length > 0 && additionalRejects.includes(trimmed.toLowerCase())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dangerous property names (case-insensitive)
|
||||||
|
if (checkBlockedNames) {
|
||||||
|
const lowerName = trimmed.toLowerCase();
|
||||||
|
if (blockedNames.some(blocked => lowerName === blocked.toLowerCase())) {
|
||||||
|
console.warn(`[RPG Companion] Blocked dangerous ${label}: "${trimmed}"`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max length check
|
||||||
|
if (trimmed.length > maxLength) {
|
||||||
|
console.warn(`[RPG Companion] ${label.charAt(0).toUpperCase() + label.slice(1)} too long (${trimmed.length} chars), truncating to ${maxLength}`);
|
||||||
|
return trimmed.slice(0, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and sanitizes storage location names.
|
* Validates and sanitizes storage location names.
|
||||||
* Prevents prototype pollution and object property shadowing attacks.
|
* Prevents prototype pollution and object property shadowing attacks.
|
||||||
@@ -35,34 +99,11 @@ const BLOCKED_PROPERTY_NAMES = [
|
|||||||
* sanitizeLocationName("__proto__") // null (blocked, logs warning)
|
* sanitizeLocationName("__proto__") // null (blocked, logs warning)
|
||||||
* sanitizeLocationName("A".repeat(300)) // "AAA..." (truncated to 200 chars)
|
* sanitizeLocationName("A".repeat(300)) // "AAA..." (truncated to 200 chars)
|
||||||
*/
|
*/
|
||||||
export function sanitizeLocationName(name) {
|
export const sanitizeLocationName = createStringValidator({
|
||||||
if (!name || typeof name !== 'string') {
|
label: 'location name',
|
||||||
return null;
|
maxLength: 200,
|
||||||
}
|
checkBlockedNames: true
|
||||||
|
});
|
||||||
const trimmed = name.trim();
|
|
||||||
|
|
||||||
// Empty check
|
|
||||||
if (trimmed === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for dangerous property names (case-insensitive)
|
|
||||||
const lowerName = trimmed.toLowerCase();
|
|
||||||
if (BLOCKED_PROPERTY_NAMES.some(blocked => lowerName === blocked.toLowerCase())) {
|
|
||||||
console.warn(`[RPG Companion] Blocked dangerous location name: "${trimmed}"`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max length check (reasonable location name)
|
|
||||||
const MAX_LOCATION_LENGTH = 200;
|
|
||||||
if (trimmed.length > MAX_LOCATION_LENGTH) {
|
|
||||||
console.warn(`[RPG Companion] Location name too long (${trimmed.length} chars), truncating to ${MAX_LOCATION_LENGTH}`);
|
|
||||||
return trimmed.slice(0, MAX_LOCATION_LENGTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and sanitizes item names.
|
* Validates and sanitizes item names.
|
||||||
@@ -76,27 +117,12 @@ export function sanitizeLocationName(name) {
|
|||||||
* sanitizeItemName("") // null
|
* sanitizeItemName("") // null
|
||||||
* sanitizeItemName("A".repeat(600)) // "AAA..." (truncated to 500 chars)
|
* sanitizeItemName("A".repeat(600)) // "AAA..." (truncated to 500 chars)
|
||||||
*/
|
*/
|
||||||
export function sanitizeItemName(name) {
|
export const sanitizeItemName = createStringValidator({
|
||||||
if (!name || typeof name !== 'string') {
|
label: 'item name',
|
||||||
return null;
|
maxLength: 500,
|
||||||
}
|
additionalRejects: ['none'],
|
||||||
|
checkBlockedNames: false
|
||||||
const trimmed = name.trim();
|
});
|
||||||
|
|
||||||
// Empty check
|
|
||||||
if (trimmed === '' || trimmed.toLowerCase() === 'none') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max length check (reasonable item name with description)
|
|
||||||
const MAX_ITEM_LENGTH = 500;
|
|
||||||
if (trimmed.length > MAX_ITEM_LENGTH) {
|
|
||||||
console.warn(`[RPG Companion] Item name too long (${trimmed.length} chars), truncating to ${MAX_ITEM_LENGTH}`);
|
|
||||||
return trimmed.slice(0, MAX_ITEM_LENGTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and cleans a stored inventory object.
|
* Validates and cleans a stored inventory object.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Fuse } from '../../../../../../lib.js';
|
import { Fuse } from '../../../../../../lib.js';
|
||||||
import {
|
import {
|
||||||
characters,
|
characters,
|
||||||
eventSource,
|
|
||||||
event_types,
|
event_types,
|
||||||
generateQuietPrompt,
|
generateQuietPrompt,
|
||||||
generateRaw,
|
generateRaw,
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
substituteParamsExtended,
|
substituteParamsExtended,
|
||||||
this_chid
|
this_chid
|
||||||
} from '../../../../../../script.js';
|
} from '../../../../../../script.js';
|
||||||
|
import { once as onceEvent } from '../core/events.js';
|
||||||
import {
|
import {
|
||||||
doExtrasFetch,
|
doExtrasFetch,
|
||||||
extension_settings as stExtensionSettings,
|
extension_settings as stExtensionSettings,
|
||||||
@@ -562,7 +562,7 @@ export async function classifyExpressionText(text, { characterName = '' } = {})
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onReady);
|
onceEvent(event_types.TEXT_COMPLETION_SETTINGS_READY, onReady);
|
||||||
|
|
||||||
const responseText = settings.promptType === PROMPT_TYPE.full
|
const responseText = settings.promptType === PROMPT_TYPE.full
|
||||||
? await generateQuietPrompt({ quietPrompt: prompt })
|
? await generateQuietPrompt({ quietPrompt: prompt })
|
||||||
|
|||||||
@@ -3292,6 +3292,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
--rpg-highlight: #ff006e;
|
--rpg-highlight: #ff006e;
|
||||||
--rpg-border: #8b00ff;
|
--rpg-border: #8b00ff;
|
||||||
--rpg-shadow: rgba(139, 0, 255, 0.5);
|
--rpg-shadow: rgba(139, 0, 255, 0.5);
|
||||||
|
--rpg-content-bg: linear-gradient(135deg, #1a1f3a 0%, #0a0e27 100%);
|
||||||
|
--rpg-content-box-shadow: 0 0 40px rgba(139, 0, 255, 0.4), inset 0 0 30px rgba(0, 0, 0, 0.5);
|
||||||
|
--rpg-content-border: 2px solid #8b00ff;
|
||||||
|
--rpg-header-text-shadow: 0 0 20px #ff006e, 0 0 40px #8b00ff;
|
||||||
|
--rpg-divider-bg: linear-gradient(to right, transparent, #8b00ff, #ff006e, #8b00ff, transparent);
|
||||||
|
--rpg-thoughts-bg: linear-gradient(135deg, rgba(10, 14, 39, 0.9) 0%, rgba(26, 31, 58, 0.8) 50%, rgba(10, 14, 39, 0.9) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply sci-fi theme to thought panel */
|
/* Apply sci-fi theme to thought panel */
|
||||||
@@ -3307,32 +3313,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
--rpg-shadow: rgba(139, 0, 255, 0.5);
|
--rpg-shadow: rgba(139, 0, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-content-box {
|
|
||||||
background: linear-gradient(135deg, #1a1f3a 0%, #0a0e27 100%);
|
|
||||||
box-shadow: 0 0 40px rgba(139, 0, 255, 0.4), inset 0 0 30px rgba(0, 0, 0, 0.5);
|
|
||||||
border: 2px solid #8b00ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-panel-header h3,
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-stats-title,
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-info-header,
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-thoughts-header {
|
|
||||||
text-shadow: 0 0 20px #ff006e, 0 0 40px #8b00ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-divider {
|
|
||||||
background: linear-gradient(to right, transparent, #8b00ff, #ff006e, #8b00ff, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-thoughts-content::before {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
rgba(10, 14, 39, 0.9) 0%,
|
|
||||||
rgba(26, 31, 58, 0.8) 50%,
|
|
||||||
rgba(10, 14, 39, 0.9) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fantasy / Rustic Parchment Theme */
|
/* Fantasy / Rustic Parchment Theme */
|
||||||
.rpg-panel[data-theme="fantasy"] {
|
.rpg-panel[data-theme="fantasy"] {
|
||||||
--rpg-bg: #2b1810;
|
--rpg-bg: #2b1810;
|
||||||
@@ -3341,6 +3321,16 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
--rpg-highlight: #d4af37;
|
--rpg-highlight: #d4af37;
|
||||||
--rpg-border: #8b6914;
|
--rpg-border: #8b6914;
|
||||||
--rpg-shadow: rgba(0, 0, 0, 0.7);
|
--rpg-shadow: rgba(0, 0, 0, 0.7);
|
||||||
|
--rpg-bg-image: linear-gradient(rgba(43, 24, 16, 0.9), rgba(43, 24, 16, 0.9)), url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" /></filter><rect width="100" height="100" filter="url(%23noise)" opacity="0.1"/></svg>');
|
||||||
|
--rpg-content-bg: linear-gradient(135deg, #3d2414 0%, #2b1810 100%);
|
||||||
|
--rpg-content-box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(139, 105, 20, 0.2);
|
||||||
|
--rpg-content-border: 3px solid #8b6914;
|
||||||
|
--rpg-content-border-style: ridge;
|
||||||
|
--rpg-header-font-family: 'Georgia', serif;
|
||||||
|
--rpg-header-text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||||
|
--rpg-divider-content: '❦';
|
||||||
|
--rpg-divider-font-size: clamp(1rem, 1.5vw, 1.5rem);
|
||||||
|
--rpg-thoughts-bg: linear-gradient(135deg, rgba(43, 24, 16, 0.92) 0%, rgba(61, 36, 20, 0.88) 50%, rgba(43, 24, 16, 0.92) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply fantasy theme to thought panel */
|
/* Apply fantasy theme to thought panel */
|
||||||
@@ -3356,41 +3346,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
--rpg-shadow: rgba(0, 0, 0, 0.7);
|
--rpg-shadow: rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] {
|
|
||||||
background-image:
|
|
||||||
linear-gradient(rgba(43, 24, 16, 0.9), rgba(43, 24, 16, 0.9)),
|
|
||||||
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4" /></filter><rect width="100" height="100" filter="url(%23noise)" opacity="0.1"/></svg>');
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-content-box {
|
|
||||||
background: linear-gradient(135deg, #3d2414 0%, #2b1810 100%);
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(139, 105, 20, 0.2);
|
|
||||||
border: 3px solid #8b6914;
|
|
||||||
border-style: ridge;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-panel-header h3,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-stats-title,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-info-header,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-thoughts-header {
|
|
||||||
font-family: 'Georgia', serif;
|
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-divider::after {
|
|
||||||
content: '❦';
|
|
||||||
font-size: clamp(1rem, 1.5vw, 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-thoughts-content::before {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
rgba(43, 24, 16, 0.92) 0%,
|
|
||||||
rgba(61, 36, 20, 0.88) 50%,
|
|
||||||
rgba(43, 24, 16, 0.92) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cyberpunk / Neon Grid Theme */
|
/* Cyberpunk / Neon Grid Theme */
|
||||||
.rpg-panel[data-theme="cyberpunk"] {
|
.rpg-panel[data-theme="cyberpunk"] {
|
||||||
--rpg-bg: #000000;
|
--rpg-bg: #000000;
|
||||||
@@ -3399,6 +3354,17 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
--rpg-highlight: #ff2a6d;
|
--rpg-highlight: #ff2a6d;
|
||||||
--rpg-border: #05d9e8;
|
--rpg-border: #05d9e8;
|
||||||
--rpg-shadow: rgba(5, 217, 232, 0.5);
|
--rpg-shadow: rgba(5, 217, 232, 0.5);
|
||||||
|
--rpg-bg-image: linear-gradient(rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.95)), repeating-linear-gradient(0deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px), repeating-linear-gradient(90deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px);
|
||||||
|
--rpg-bg-size: 100% 100%, 30px 30px, 30px 30px;
|
||||||
|
--rpg-content-bg: linear-gradient(135deg, rgba(13, 13, 13, 0.9) 0%, rgba(0, 0, 0, 0.9) 100%);
|
||||||
|
--rpg-content-box-shadow: 0 0 40px rgba(255, 42, 109, 0.4), inset 0 0 30px rgba(5, 217, 232, 0.2);
|
||||||
|
--rpg-content-border: 2px solid #05d9e8;
|
||||||
|
--rpg-header-font-family: 'Courier New', monospace;
|
||||||
|
--rpg-header-letter-spacing: 0.125em;
|
||||||
|
--rpg-header-text-shadow: 0 0 10px #ff2a6d, 0 0 20px #05d9e8, 0 0 30px #ff2a6d;
|
||||||
|
--rpg-divider-bg: linear-gradient(to right, transparent, #05d9e8, #ff2a6d, #05d9e8, transparent);
|
||||||
|
--rpg-divider-height: 0.188rem;
|
||||||
|
--rpg-thoughts-bg: linear-gradient(135deg, rgba(0, 0, 0, 0.92) 0%, rgba(13, 13, 13, 0.85) 50%, rgba(0, 0, 0, 0.92) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply cyberpunk theme to thought panel */
|
/* Apply cyberpunk theme to thought panel */
|
||||||
@@ -3414,253 +3380,184 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
--rpg-shadow: rgba(5, 217, 232, 0.5);
|
--rpg-shadow: rgba(5, 217, 232, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] {
|
|
||||||
background: linear-gradient(rgba(0, 0, 0, 0.95), rgba(0, 0, 0, 0.95)),
|
|
||||||
repeating-linear-gradient(0deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px),
|
|
||||||
repeating-linear-gradient(90deg, rgba(5, 217, 232, 0.1) 0px, transparent 1px, transparent 2px, rgba(5, 217, 232, 0.1) 3px);
|
|
||||||
background-size: 100% 100%, 30px 30px, 30px 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-content-box {
|
|
||||||
background: linear-gradient(135deg, rgba(13, 13, 13, 0.9) 0%, rgba(0, 0, 0, 0.9) 100%);
|
|
||||||
box-shadow: 0 0 40px rgba(255, 42, 109, 0.4), inset 0 0 30px rgba(5, 217, 232, 0.2);
|
|
||||||
border: 2px solid #05d9e8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-panel-header h3,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-stats-title,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-info-header,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-thoughts-header {
|
|
||||||
text-shadow: 0 0 10px #ff2a6d, 0 0 20px #05d9e8, 0 0 30px #ff2a6d;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
letter-spacing: 0.125em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-divider {
|
|
||||||
background: linear-gradient(to right, transparent, #05d9e8, #ff2a6d, #05d9e8, transparent);
|
|
||||||
height: 0.188rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-thoughts-content::before {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
rgba(0, 0, 0, 0.92) 0%,
|
|
||||||
rgba(13, 13, 13, 0.85) 50%,
|
|
||||||
rgba(0, 0, 0, 0.92) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
THEME SUPPORT FOR ALL PANEL ELEMENTS
|
THEME SUPPORT FOR ALL PANEL ELEMENTS
|
||||||
|
Uses CSS custom properties defined above — no selector duplication needed.
|
||||||
|
Any element inside .rpg-panel inherits the theme variables automatically.
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
/* Apply theme colors to tabs navigation */
|
/* Apply theme background image if defined */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-tabs-nav,
|
.rpg-panel[data-theme] {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-tabs-nav,
|
background-image: var(--rpg-bg-image, none);
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-tabs-nav {
|
background-size: var(--rpg-bg-size, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content box theming via custom properties */
|
||||||
|
.rpg-panel[data-theme] .rpg-content-box {
|
||||||
|
background: var(--rpg-content-bg);
|
||||||
|
box-shadow: var(--rpg-content-box-shadow);
|
||||||
|
border: var(--rpg-content-border);
|
||||||
|
border-style: var(--rpg-content-border-style, solid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header theming */
|
||||||
|
.rpg-panel[data-theme] .rpg-panel-header h3,
|
||||||
|
.rpg-panel[data-theme] .rpg-stats-title,
|
||||||
|
.rpg-panel[data-theme] .rpg-info-header,
|
||||||
|
.rpg-panel[data-theme] .rpg-thoughts-header {
|
||||||
|
font-family: var(--rpg-header-font-family, inherit);
|
||||||
|
text-shadow: var(--rpg-header-text-shadow, none);
|
||||||
|
letter-spacing: var(--rpg-header-letter-spacing, normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Divider theming */
|
||||||
|
.rpg-panel[data-theme] .rpg-divider {
|
||||||
|
background: var(--rpg-divider-bg);
|
||||||
|
height: var(--rpg-divider-height, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-panel[data-theme] .rpg-divider::after {
|
||||||
|
content: var(--rpg-divider-content, none);
|
||||||
|
font-size: var(--rpg-divider-font-size, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Thoughts content theming */
|
||||||
|
.rpg-panel[data-theme] .rpg-thoughts-content::before {
|
||||||
|
background: var(--rpg-thoughts-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs navigation theming */
|
||||||
|
.rpg-panel[data-theme] .rpg-tabs-nav {
|
||||||
border-bottom-color: var(--rpg-border);
|
border-bottom-color: var(--rpg-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-tab-btn,
|
.rpg-panel[data-theme] .rpg-tab-btn {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-tab-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-tab-btn {
|
|
||||||
background: var(--rpg-accent);
|
background: var(--rpg-accent);
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-tab-btn:hover,
|
.rpg-panel[data-theme] .rpg-tab-btn:hover {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-tab-btn:hover,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-tab-btn:hover {
|
|
||||||
background: var(--rpg-highlight);
|
background: var(--rpg-highlight);
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-tab-btn.active,
|
.rpg-panel[data-theme] .rpg-tab-btn.active {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-tab-btn.active,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-tab-btn.active {
|
|
||||||
background: var(--rpg-highlight);
|
background: var(--rpg-highlight);
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-bg);
|
color: var(--rpg-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to inventory subtabs */
|
/* Inventory subtabs theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtabs,
|
.rpg-panel[data-theme] .rpg-inventory-subtabs {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtabs,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtabs {
|
|
||||||
border-bottom-color: var(--rpg-border);
|
border-bottom-color: var(--rpg-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtab,
|
.rpg-panel[data-theme] .rpg-inventory-subtab {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtab,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtab {
|
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtab:hover,
|
.rpg-panel[data-theme] .rpg-inventory-subtab:hover {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtab:hover,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtab:hover {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-subtab.active,
|
.rpg-panel[data-theme] .rpg-inventory-subtab.active {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-subtab.active,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-subtab.active {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to quests subtabs */
|
/* Quests subtabs theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtabs,
|
.rpg-panel[data-theme] .rpg-quests-subtabs {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtabs,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtabs {
|
|
||||||
border-bottom-color: var(--rpg-border);
|
border-bottom-color: var(--rpg-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtab,
|
.rpg-panel[data-theme] .rpg-quests-subtab {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtab,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtab {
|
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtab:hover,
|
.rpg-panel[data-theme] .rpg-quests-subtab:hover {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtab:hover,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtab:hover {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quests-subtab.active,
|
.rpg-panel[data-theme] .rpg-quests-subtab.active {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quests-subtab.active,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quests-subtab.active {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to storage locations */
|
/* Storage locations theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-storage-location,
|
.rpg-panel[data-theme] .rpg-storage-location {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-storage-location,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-storage-location {
|
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-storage-header,
|
.rpg-panel[data-theme] .rpg-storage-header {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-storage-header,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-storage-header {
|
|
||||||
background: var(--rpg-highlight);
|
background: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-storage-content,
|
.rpg-panel[data-theme] .rpg-storage-content {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-storage-content,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-storage-content {
|
|
||||||
background: var(--rpg-accent);
|
background: var(--rpg-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to buttons */
|
/* Button theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-edit-btn,
|
.rpg-panel[data-theme] .rpg-inventory-edit-btn,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-add-btn,
|
.rpg-panel[data-theme] .rpg-inventory-add-btn,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-remove-btn,
|
.rpg-panel[data-theme] .rpg-inventory-remove-btn,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit,
|
.rpg-panel[data-theme] .rpg-quest-edit,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-remove,
|
.rpg-panel[data-theme] .rpg-quest-remove,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-add-quest-btn,
|
.rpg-panel[data-theme] .rpg-add-quest-btn {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-edit-btn,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-add-btn,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-remove-btn,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-remove,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-add-quest-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-edit-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-add-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-remove-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-remove,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-add-quest-btn {
|
|
||||||
background: var(--rpg-accent);
|
background: var(--rpg-accent);
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-add-btn,
|
.rpg-panel[data-theme] .rpg-inventory-add-btn,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-add-quest-btn,
|
.rpg-panel[data-theme] .rpg-add-quest-btn {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-add-btn,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-add-quest-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-add-btn,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-add-quest-btn {
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-highlight);
|
color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to inventory/quest items */
|
/* Inventory/quest items theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-text,
|
.rpg-panel[data-theme] .rpg-inventory-text,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-item,
|
.rpg-panel[data-theme] .rpg-quest-item,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-main-quest-display,
|
.rpg-panel[data-theme] .rpg-main-quest-display {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-text,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-item,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-main-quest-display,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-text,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-item,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-main-quest-display {
|
|
||||||
background: var(--rpg-accent);
|
background: var(--rpg-accent);
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-item:hover,
|
.rpg-panel[data-theme] .rpg-quest-item:hover {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-item:hover,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-item:hover {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to hints and empty states */
|
/* Hints and empty states theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-hint,
|
.rpg-panel[data-theme] .rpg-inventory-hint,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-hint,
|
.rpg-panel[data-theme] .rpg-quest-hint,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inventory-empty,
|
.rpg-panel[data-theme] .rpg-inventory-empty,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-empty,
|
.rpg-panel[data-theme] .rpg-quest-empty {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-hint,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-hint,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inventory-empty,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-empty,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-hint,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-hint,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inventory-empty,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-empty {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply theme colors to input fields */
|
/* Input fields theming */
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inline-input,
|
.rpg-panel[data-theme] .rpg-inline-input,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form input,
|
.rpg-panel[data-theme] .rpg-quest-edit-form input,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form textarea,
|
.rpg-panel[data-theme] .rpg-quest-edit-form textarea {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inline-input,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form input,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form textarea,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inline-input,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form input,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form textarea {
|
|
||||||
background: var(--rpg-bg);
|
background: var(--rpg-bg);
|
||||||
border-color: var(--rpg-border);
|
border-color: var(--rpg-border);
|
||||||
color: var(--rpg-text);
|
color: var(--rpg-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-inline-input:focus,
|
.rpg-panel[data-theme] .rpg-inline-input:focus,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form input:focus,
|
.rpg-panel[data-theme] .rpg-quest-edit-form input:focus,
|
||||||
.rpg-panel[data-theme="sci-fi"] .rpg-quest-edit-form textarea:focus,
|
.rpg-panel[data-theme] .rpg-quest-edit-form textarea:focus {
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-inline-input:focus,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form input:focus,
|
|
||||||
.rpg-panel[data-theme="fantasy"] .rpg-quest-edit-form textarea:focus,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-inline-input:focus,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form input:focus,
|
|
||||||
.rpg-panel[data-theme="cyberpunk"] .rpg-quest-edit-form textarea:focus {
|
|
||||||
border-color: var(--rpg-highlight);
|
border-color: var(--rpg-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
/* _09_dice-modal.css */
|
/* _09_dice-modal.css */
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
@@ -6881,8 +6778,6 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
content-visibility: auto;
|
|
||||||
contain-intrinsic-size: 0 500px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sub-tabs Navigation */
|
/* Sub-tabs Navigation */
|
||||||
@@ -12081,8 +11976,6 @@ body.documentstyle .rpg-inline-thoughts {
|
|||||||
|
|
||||||
.rpg-equipment-container {
|
.rpg-equipment-container {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
content-visibility: auto;
|
|
||||||
contain-intrinsic-size: 0 300px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-equipment-header {
|
.rpg-equipment-header {
|
||||||
|
|||||||
+1
-1
@@ -1114,7 +1114,7 @@
|
|||||||
Injected when "Enable Dialogue Coloring" is enabled. Affects all generation modes.
|
Injected when "Enable Dialogue Coloring" is enabled. Affects all generation modes.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-dialogue-coloring" class="rpg-prompt-textarea" rows="4"></textarea>
|
<textarea id="rpg-prompt-dialogue-coloring" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="dialogue-coloring" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="dialogueColoring" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user