Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 105e20e97a | |||
| 5498c64f5d | |||
| 5fa369e3d7 | |||
| 52be8dca1f | |||
| 32c4f67822 | |||
| b61a426efe | |||
| 2a77c091dd | |||
| c0431a6117 | |||
| 43610bf8b6 | |||
| 2a5b57087b | |||
| 653d23ef9a | |||
| ea81dd0634 | |||
| 7a3487c741 | |||
| 6fc35e50a1 | |||
| 08474bd910 | |||
| 0bb2085305 | |||
| c6f13d18ff | |||
| 334f5fa5a3 | |||
| 5f9d67ebe8 | |||
| 93c37c25d7 |
@@ -7,13 +7,12 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
|
||||
|
||||
## 🆕 What's New
|
||||
|
||||
### v3.6.2
|
||||
### v3.7.2
|
||||
|
||||
- Various bug fixes.
|
||||
- Added the ability to add present characters manually.
|
||||
- Minor bug fixes
|
||||
|
||||
**Special thanks to all the other contributors for this project:**
|
||||
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
||||
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein!
|
||||
|
||||
## 📥 Installation
|
||||
|
||||
@@ -266,7 +265,7 @@ If you enjoy this extension, consider supporting development:
|
||||
## 🙏 Credits
|
||||
|
||||
**Contributors:**
|
||||
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
||||
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein.
|
||||
|
||||
## 🚀 Planned Features
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js';
|
||||
import { eventSource, event_types, substituteParams, chat, generateRaw, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js';
|
||||
import { eventSource, event_types, substituteParams, chat, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js';
|
||||
import { selected_group, getGroupMembers } from '../../../group-chats.js';
|
||||
import { power_user } from '../../../power-user.js';
|
||||
|
||||
@@ -384,6 +384,11 @@ async function initUI() {
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-omniscience').on('change', function() {
|
||||
extensionSettings.enableOmniscienceFilter = $(this).prop('checked');
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#rpg-toggle-cyoa').on('change', function() {
|
||||
extensionSettings.enableCYOA = $(this).prop('checked');
|
||||
saveSettings();
|
||||
@@ -572,6 +577,12 @@ async function initUI() {
|
||||
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();
|
||||
@@ -805,12 +816,30 @@ async function initUI() {
|
||||
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());
|
||||
@@ -832,6 +861,19 @@ async function initUI() {
|
||||
}
|
||||
});
|
||||
|
||||
$('#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();
|
||||
@@ -842,6 +884,19 @@ async function initUI() {
|
||||
}
|
||||
});
|
||||
|
||||
$('#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();
|
||||
@@ -852,6 +907,19 @@ async function initUI() {
|
||||
}
|
||||
});
|
||||
|
||||
$('#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();
|
||||
@@ -862,6 +930,19 @@ async function initUI() {
|
||||
}
|
||||
});
|
||||
|
||||
$('#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) {
|
||||
@@ -969,6 +1050,7 @@ async function initUI() {
|
||||
$('#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);
|
||||
|
||||
@@ -979,6 +1061,7 @@ async function initUI() {
|
||||
$('#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);
|
||||
@@ -1041,12 +1124,29 @@ async function initUI() {
|
||||
$('#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) {
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Marinara",
|
||||
"version": "3.6.3",
|
||||
"version": "3.7.2",
|
||||
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
||||
}
|
||||
|
||||
+2
-2
@@ -44,12 +44,12 @@
|
||||
<i class="fa-solid fa-users"></i> <strong>Contributors:</strong>
|
||||
</div>
|
||||
<div style="opacity: 0.8; font-size: 0.9em;">
|
||||
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
||||
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
|
||||
v3.6.2
|
||||
v3.7.2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,6 +118,22 @@ export function loadSettings() {
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
// Migration to version 5: Add opacity properties for all colors
|
||||
if (currentVersion < 5) {
|
||||
// console.log('[RPG Companion] Migrating settings to version 5 (adding color opacity)');
|
||||
if (!extensionSettings.customColors) {
|
||||
extensionSettings.customColors = {};
|
||||
}
|
||||
if (extensionSettings.customColors.bgOpacity === undefined) extensionSettings.customColors.bgOpacity = 100;
|
||||
if (extensionSettings.customColors.accentOpacity === undefined) extensionSettings.customColors.accentOpacity = 100;
|
||||
if (extensionSettings.customColors.textOpacity === undefined) extensionSettings.customColors.textOpacity = 100;
|
||||
if (extensionSettings.customColors.highlightOpacity === undefined) extensionSettings.customColors.highlightOpacity = 100;
|
||||
if (extensionSettings.statBarColorLowOpacity === undefined) extensionSettings.statBarColorLowOpacity = 100;
|
||||
if (extensionSettings.statBarColorHighOpacity === undefined) extensionSettings.statBarColorHighOpacity = 100;
|
||||
extensionSettings.settingsVersion = 5;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
// Save migrated settings
|
||||
if (settingsChanged) {
|
||||
saveSettings();
|
||||
|
||||
+11
-1
@@ -23,12 +23,15 @@ export let extensionSettings = {
|
||||
showThoughtsInChat: true, // Show thoughts overlay in chat
|
||||
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 <filter> 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)
|
||||
@@ -41,6 +44,7 @@ export let extensionSettings = {
|
||||
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
|
||||
|
||||
@@ -62,12 +66,18 @@ export let extensionSettings = {
|
||||
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
||||
customColors: {
|
||||
bg: '#1a1a2e',
|
||||
bgOpacity: 100,
|
||||
accent: '#16213e',
|
||||
accentOpacity: 100,
|
||||
text: '#eaeaea',
|
||||
highlight: '#e94560'
|
||||
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)',
|
||||
|
||||
+7
-1
@@ -52,6 +52,10 @@
|
||||
"template.settingsModal.display.showImmersiveHtmlToggleNote": "Display a toggle button to enable/disable HTML formatting in messages.",
|
||||
"template.settingsModal.display.showDialogueColoringToggle": "Show Colored Dialogues",
|
||||
"template.settingsModal.display.showDialogueColoringToggleNote": "Display a toggle button to enable/disable colored dialogue formatting.",
|
||||
"template.settingsModal.display.showDeceptionToggle": "Show Deception System",
|
||||
"template.settingsModal.display.showDeceptionToggleNote": "Display a toggle button to enable/disable the Deception System for marking lies and deceptions.",
|
||||
"template.settingsModal.display.showOmniscienceToggle": "Show Omniscience Filter",
|
||||
"template.settingsModal.display.showOmniscienceToggleNote": "Display a toggle button to enable/disable the Omniscience Filter for filtering hidden events.",
|
||||
"template.settingsModal.display.showSpotifyMusicToggle": "Show Spotify Music",
|
||||
"template.settingsModal.display.showSpotifyMusicToggleNote": "Display Spotify music player with AI-suggested scene-appropriate tracks.",
|
||||
"template.settingsModal.display.showSnowflakesToggle": "Show Snowflakes Effect",
|
||||
@@ -155,13 +159,15 @@
|
||||
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:",
|
||||
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats",
|
||||
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track Character Stats",
|
||||
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored bars).",
|
||||
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored numbers).",
|
||||
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat",
|
||||
"template.mainPanel.title": "RPG Companion",
|
||||
"template.mainPanel.lastRoll": "Last Roll:",
|
||||
"template.mainPanel.clearLastRoll": "Clear last roll",
|
||||
"template.mainPanel.immersiveHtml": "Immersive HTML",
|
||||
"template.mainPanel.coloredDialogues": "Colored Dialogues",
|
||||
"template.mainPanel.deceptionSystem": "Deception System",
|
||||
"template.mainPanel.omniscienceFilter": "Omniscience Filter",
|
||||
"template.mainPanel.spotifyMusic": "Spotify Music",
|
||||
"template.mainPanel.snowflakesEffect": "Snowflakes Effect",
|
||||
"template.mainPanel.dynamicWeatherEffects": "Dynamic Weather",
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
* - Manual regeneration support
|
||||
*/
|
||||
|
||||
import { generateRaw, characters, this_chid } from '../../../../../../../script.js';
|
||||
import { characters, this_chid } from '../../../../../../../script.js';
|
||||
import { safeGenerateRaw } from '../../utils/responseExtractor.js';
|
||||
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
||||
import { selected_group, getGroupMembers } from '../../../../../../group-chats.js';
|
||||
import { extensionSettings, sessionAvatarPrompts, setSessionAvatarPrompt } from '../../core/state.js';
|
||||
@@ -254,7 +255,7 @@ async function generateAvatarPrompt(characterName) {
|
||||
// console.log('[RPG Avatar] Using external API for avatar prompt generation');
|
||||
response = await generateWithExternalAPI(promptMessages);
|
||||
} else {
|
||||
response = await generateRaw({
|
||||
response = await safeGenerateRaw({
|
||||
prompt: promptMessages,
|
||||
quietToLoud: false
|
||||
});
|
||||
|
||||
@@ -20,6 +20,10 @@ export function setupClassicStatsButtons() {
|
||||
// Delegated event listener for increase buttons
|
||||
$userStatsContainer.on('click', '.rpg-stat-increase', function() {
|
||||
const stat = $(this).data('stat');
|
||||
// Initialize custom attributes if they don't exist
|
||||
if (extensionSettings.classicStats[stat] === undefined) {
|
||||
extensionSettings.classicStats[stat] = 10;
|
||||
}
|
||||
if (extensionSettings.classicStats[stat] < 999) {
|
||||
extensionSettings.classicStats[stat]++;
|
||||
saveSettings();
|
||||
@@ -33,6 +37,10 @@ export function setupClassicStatsButtons() {
|
||||
// Delegated event listener for decrease buttons
|
||||
$userStatsContainer.on('click', '.rpg-stat-decrease', function() {
|
||||
const stat = $(this).data('stat');
|
||||
// Initialize custom attributes if they don't exist
|
||||
if (extensionSettings.classicStats[stat] === undefined) {
|
||||
extensionSettings.classicStats[stat] = 10;
|
||||
}
|
||||
if (extensionSettings.classicStats[stat] > 1) {
|
||||
extensionSettings.classicStats[stat]--;
|
||||
saveSettings();
|
||||
|
||||
@@ -76,7 +76,7 @@ export async function ensureJsonCleaningRegex(st_extension_settings, saveSetting
|
||||
}
|
||||
// Small delay to ensure save completes
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.6 settings.');
|
||||
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.3 settings.');
|
||||
} else {
|
||||
console.log('[RPG Companion] JSON Cleaning Regex is up to date.');
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* Handles API calls for RPG tracker generation
|
||||
*/
|
||||
|
||||
import { generateRaw, chat, eventSource } from '../../../../../../../script.js';
|
||||
import { chat, eventSource } from '../../../../../../../script.js';
|
||||
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
||||
import { safeGenerateRaw, extractTextFromResponse } from '../../utils/responseExtractor.js';
|
||||
|
||||
// Custom event name for when RPG Companion finishes updating tracker data
|
||||
// Other extensions can listen for this event to know when RPG Companion is done
|
||||
@@ -107,11 +108,10 @@ export async function generateWithExternalAPI(messages) {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
|
||||
throw new Error('Invalid response format from external API');
|
||||
const content = extractTextFromResponse(data);
|
||||
if (!content || !content.trim()) {
|
||||
throw new Error('Invalid response format from external API — no text content found');
|
||||
}
|
||||
|
||||
const content = data.choices[0].message.content;
|
||||
// console.log('[RPG Companion] External API response received successfully');
|
||||
|
||||
return content;
|
||||
@@ -255,8 +255,8 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
// console.log('[RPG Companion] Using external API for tracker generation');
|
||||
response = await generateWithExternalAPI(prompt);
|
||||
} else {
|
||||
// Separate mode: Use SillyTavern's generateRaw
|
||||
response = await generateRaw({
|
||||
// Separate mode: Use SillyTavern's generateRaw (with extended thinking fallback)
|
||||
response = await safeGenerateRaw({
|
||||
prompt: prompt,
|
||||
quietToLoud: false
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles, eventSource, event_types } from '../../../../../../../script.js';
|
||||
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, eventSource, event_types } from '../../../../../../../script.js';
|
||||
import {
|
||||
extensionSettings,
|
||||
committedTrackerData,
|
||||
@@ -22,8 +22,11 @@ import {
|
||||
DEFAULT_HTML_PROMPT,
|
||||
DEFAULT_DIALOGUE_COLORING_PROMPT,
|
||||
DEFAULT_DECEPTION_PROMPT,
|
||||
DEFAULT_OMNISCIENCE_FILTER_PROMPT,
|
||||
DEFAULT_CYOA_PROMPT,
|
||||
DEFAULT_SPOTIFY_PROMPT,
|
||||
DEFAULT_NARRATOR_PROMPT,
|
||||
DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
|
||||
SPOTIFY_FORMAT_INSTRUCTION
|
||||
} from './promptBuilder.js';
|
||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||
@@ -478,6 +481,7 @@ function onGenerateBeforeCombinePrompts(eventData) {
|
||||
/**
|
||||
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
|
||||
* This is now a backup/fallback - primary injection happens in BEFORE_COMBINE.
|
||||
* Also fixes newline spacing after </context> tag.
|
||||
*
|
||||
* @param {Object} eventData - Event data with prompt property
|
||||
*/
|
||||
@@ -490,24 +494,25 @@ function onGenerateAfterCombinePrompts(eventData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if injection already happened in BEFORE_COMBINE
|
||||
if (historyInjectionDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only inject if we have pending context
|
||||
if (pendingContextMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
let didInjectHistory = false;
|
||||
|
||||
// Inject historical context if available and not already done
|
||||
if (!historyInjectionDone && pendingContextMap.size > 0) {
|
||||
// Fallback injection for edge cases where BEFORE_COMBINE didn't work
|
||||
console.log('[RPG Companion] Using fallback string-based injection (AFTER_COMBINE)');
|
||||
eventData.prompt = injectContextIntoTextPrompt(eventData.prompt);
|
||||
didInjectHistory = true;
|
||||
}
|
||||
|
||||
// Always fix newlines around context tags (whether we just injected or not)
|
||||
eventData.prompt = eventData.prompt.replace(/<context>/g, '\n<context>');
|
||||
eventData.prompt = eventData.prompt.replace(/<\/context>/g, '</context>\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for CHAT_COMPLETION_PROMPT_READY.
|
||||
* Injects historical context into the chat message array.
|
||||
* Also fixes newline spacing around <context> tags.
|
||||
*
|
||||
* @param {Object} eventData - Event data with chat property
|
||||
*/
|
||||
@@ -520,16 +525,22 @@ function onChatCompletionPromptReady(eventData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only inject if we have pending context
|
||||
if (pendingContextMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inject historical context if we have pending context
|
||||
if (pendingContextMap.size > 0) {
|
||||
eventData.chat = injectContextIntoChatPrompt(eventData.chat);
|
||||
// DON'T clear pendingContextMap here - let it persist for other generations
|
||||
// (e.g., prewarm extensions). It will be cleared on GENERATION_ENDED.
|
||||
}
|
||||
|
||||
// Fix newlines around context tags for all messages
|
||||
for (const message of eventData.chat) {
|
||||
if (message.content && typeof message.content === 'string') {
|
||||
message.content = message.content.replace(/<context>/g, '\n<context>');
|
||||
message.content = message.content.replace(/<\/context>/g, '</context>\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for generation start.
|
||||
* Manages tracker data commitment and prompt injection based on generation mode.
|
||||
@@ -792,6 +803,19 @@ export async function onGenerationStarted(type, data, dryRun) {
|
||||
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Omniscience Filter prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableOmniscienceFilter && !shouldSuppress) {
|
||||
// Use custom Omniscience Filter prompt if set, otherwise use default
|
||||
const omnisciencePromptText = extensionSettings.customOmnisciencePrompt || DEFAULT_OMNISCIENCE_FILTER_PROMPT;
|
||||
const omnisciencePrompt = `\n${omnisciencePromptText}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-omniscience', omnisciencePrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Omniscience Filter prompt at depth 0 for together mode');
|
||||
} else {
|
||||
// Clear Omniscience Filter prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Spotify prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
||||
// Use custom Spotify prompt if set, otherwise use default
|
||||
@@ -823,12 +847,14 @@ export async function onGenerationStarted(type, data, dryRun) {
|
||||
const contextSummary = generateContextualSummary();
|
||||
|
||||
if (contextSummary) {
|
||||
const wrappedContext = `\nHere is context information about the current scene, and what follows is the last message in the chat history:
|
||||
// Use custom context instructions prompt if set, otherwise use default
|
||||
const contextInstructionsText = extensionSettings.customContextInstructionsPrompt || DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT;
|
||||
|
||||
const wrappedContext = `
|
||||
<context>
|
||||
${contextSummary}
|
||||
|
||||
Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses.
|
||||
</context>\n\n`;
|
||||
${contextInstructionsText}
|
||||
</context>`;
|
||||
|
||||
// Inject context at depth 1 (before last user message) as SYSTEM
|
||||
// Skip when a guided generation injection is present to avoid conflicting instructions
|
||||
@@ -880,6 +906,19 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Omniscience Filter prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableOmniscienceFilter && !shouldSuppress) {
|
||||
// Use custom Omniscience Filter prompt if set, otherwise use default
|
||||
const omnisciencePromptText = extensionSettings.customOmnisciencePrompt || DEFAULT_OMNISCIENCE_FILTER_PROMPT;
|
||||
const omnisciencePrompt = `\n${omnisciencePromptText}\n`;
|
||||
|
||||
setExtensionPrompt('rpg-companion-omniscience', omnisciencePrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||
// console.log('[RPG Companion] Injected Omniscience Filter prompt at depth 0 for separate/external mode');
|
||||
} else {
|
||||
// Clear Omniscience Filter prompt if disabled
|
||||
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
// Inject Spotify prompt separately at depth 0 if enabled
|
||||
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
||||
// Use custom Spotify prompt if set, otherwise use default
|
||||
@@ -917,6 +956,7 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import { getWeatherKeywordsAsPromptString } from '../ui/weatherEffects.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
|
||||
/**
|
||||
* Converts a field name to snake_case for use as JSON key
|
||||
@@ -19,6 +21,19 @@ function toSnakeCase(name) {
|
||||
.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
|
||||
* Parenthetical content is treated as a description/hint, not part of the key.
|
||||
* Example: "Conditions (up to 5 traits)" -> "conditions"
|
||||
* Example: "Status Effects" -> "status_effects"
|
||||
* @param {string} name - Field name, possibly with parenthetical description
|
||||
* @returns {string} snake_case key from the base name only
|
||||
*/
|
||||
function toFieldKey(name) {
|
||||
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
||||
return toSnakeCase(baseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds User Stats JSON format instruction
|
||||
* @returns {string} JSON format instruction for user stats
|
||||
@@ -58,12 +73,12 @@ export function buildUserStatsJSONInstruction() {
|
||||
if (customFields.length > 0) {
|
||||
for (let i = 0; i < customFields.length; i++) {
|
||||
const fieldName = customFields[i].toLowerCase();
|
||||
const fieldKey = toSnakeCase(fieldName);
|
||||
const fieldKey = toFieldKey(fieldName);
|
||||
const comma = (i === customFields.length - 1 && !userStatsConfig.statusSection.showMoodEmoji) ? '' : (userStatsConfig.statusSection.showMoodEmoji || i < customFields.length - 1 ? ',\n' : '\n');
|
||||
if (i === 0 && userStatsConfig.statusSection.showMoodEmoji) {
|
||||
instruction += ',\n';
|
||||
}
|
||||
instruction += ` "${fieldKey}": "[${fieldName}1, ${fieldName}2]"${comma}`;
|
||||
instruction += ` "${fieldKey}": "[${fieldName}]"${comma}`;
|
||||
}
|
||||
}
|
||||
if (!userStatsConfig.statusSection.showMoodEmoji && customFields.length > 0) {
|
||||
@@ -132,7 +147,10 @@ export function buildInfoBoxJSONInstruction() {
|
||||
}
|
||||
|
||||
if (widgets.weather?.enabled) {
|
||||
instruction += (hasFields ? ',\n' : '') + ' "weather": {"emoji": "Weather Emoji", "forecast": "Forecast"}';
|
||||
// Get valid weather keywords for the current language to guide LLM generation
|
||||
const currentLang = i18n.currentLanguage || 'en';
|
||||
const weatherHint = getWeatherKeywordsAsPromptString(currentLang);
|
||||
instruction += (hasFields ? ',\n' : '') + ` "weather": {"emoji": "Weather Emoji", "forecast": "Forecast"} // ${weatherHint}`;
|
||||
hasFields = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,16 +98,19 @@ function applyUserStatsLocks(data, lockedItems) {
|
||||
}
|
||||
}
|
||||
|
||||
// Lock inventory items - handle bracket notation paths like "inventory.onPerson[0]"
|
||||
// Lock inventory items - match by item name instead of index
|
||||
if (data.inventory && lockedItems.inventory) {
|
||||
// Helper function to parse bracket notation and apply lock
|
||||
// Helper function to apply locks based on item name
|
||||
const applyInventoryLocks = (items, category) => {
|
||||
if (!Array.isArray(items)) return items;
|
||||
if (!lockedItems.inventory[category]) return items;
|
||||
|
||||
return items.map((item, index) => {
|
||||
// Check if this specific item is locked using bracket notation with inventory prefix
|
||||
const bracketPath = `${category}[${index}]`;
|
||||
if (lockedItems.inventory[bracketPath]) {
|
||||
return items.map((item) => {
|
||||
// Get item name (handle both string and object formats)
|
||||
const itemName = typeof item === 'string' ? item : (item.item || item.name || '');
|
||||
|
||||
// Check if this specific item name is locked
|
||||
if (lockedItems.inventory[category][itemName]) {
|
||||
return typeof item === 'string'
|
||||
? { item, locked: true }
|
||||
: { ...item, locked: true };
|
||||
@@ -131,13 +134,13 @@ function applyUserStatsLocks(data, lockedItems) {
|
||||
data.inventory.assets = applyInventoryLocks(data.inventory.assets, 'assets');
|
||||
}
|
||||
|
||||
// Apply locks to stored items (nested structure with inventory.stored.location[index])
|
||||
// Apply locks to stored items - match by item name
|
||||
if (data.inventory.stored && lockedItems.inventory.stored) {
|
||||
for (const location in data.inventory.stored) {
|
||||
if (Array.isArray(data.inventory.stored[location])) {
|
||||
data.inventory.stored[location] = data.inventory.stored[location].map((item, index) => {
|
||||
const bracketPath = `${location}[${index}]`;
|
||||
if (lockedItems.inventory.stored[bracketPath]) {
|
||||
if (Array.isArray(data.inventory.stored[location]) && lockedItems.inventory.stored[location]) {
|
||||
data.inventory.stored[location] = data.inventory.stored[location].map((item) => {
|
||||
const itemName = typeof item === 'string' ? item : (item.item || item.name || '');
|
||||
if (lockedItems.inventory.stored[location][itemName]) {
|
||||
return typeof item === 'string'
|
||||
? { item, locked: true }
|
||||
: { ...item, locked: true };
|
||||
|
||||
@@ -9,6 +9,20 @@ import { saveSettings } from '../../core/persistence.js';
|
||||
import { extractInventory } from './inventoryParser.js';
|
||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||
|
||||
/**
|
||||
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
|
||||
* Example: "Conditions (up to 5 traits)" -> "conditions"
|
||||
* @param {string} name - Field name, possibly with parenthetical description
|
||||
* @returns {string} snake_case key from the base name only
|
||||
*/
|
||||
function toFieldKey(name) {
|
||||
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
||||
return baseName
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to separate emoji from text in a string
|
||||
* Handles cases where there's no comma or space after emoji
|
||||
@@ -559,10 +573,12 @@ export function parseUserStats(statsText) {
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
const customFields = trackerConfig?.userStats?.statusSection?.customFields || [];
|
||||
for (const fieldName of customFields) {
|
||||
const fieldKey = fieldName.toLowerCase();
|
||||
if (statsData.status[fieldKey]) {
|
||||
extensionSettings.userStats[fieldKey] = statsData.status[fieldKey];
|
||||
// console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, statsData.status[fieldKey]);
|
||||
const fieldKey = toFieldKey(fieldName);
|
||||
// Try the base key first (e.g., "conditions"), then fall back to full lowercase name
|
||||
const value = statsData.status[fieldKey] || statsData.status[fieldName.toLowerCase()];
|
||||
if (value) {
|
||||
extensionSettings.userStats[fieldKey] = value;
|
||||
// console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -617,6 +633,13 @@ export function parseUserStats(statsText) {
|
||||
if (!quest) return '';
|
||||
if (typeof quest === 'string') return quest;
|
||||
if (typeof quest === 'object') {
|
||||
// Check for locked format: {value, locked}
|
||||
// Recursively extract value if it's nested
|
||||
let extracted = quest;
|
||||
while (typeof extracted === 'object' && extracted.value !== undefined) {
|
||||
extracted = extracted.value;
|
||||
}
|
||||
if (typeof extracted === 'string') return extracted;
|
||||
// v3 format: {title, description, status}
|
||||
return quest.title || quest.description || JSON.stringify(quest);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,15 @@ export const DEFAULT_DIALOGUE_COLORING_PROMPT = `Wrap all character/NPC "dialogu
|
||||
*/
|
||||
export const DEFAULT_DECEPTION_PROMPT = `When a character is lying or deceiving, you should follow up that line with the <lie> tag, containing a brief description of the truth and the lie's reason, using the template below (replace placeholders in quotation marks). This will be hidden from the user's view, but not to you, making it useful for future consequences: <lie character="name" type="lying/deceiving/omitting" truth="truth" reason="reason"/>.`;
|
||||
|
||||
/**
|
||||
* Default Omniscience Filter prompt text
|
||||
* This instructs the AI to separate information the player character cannot perceive
|
||||
*/
|
||||
export const DEFAULT_OMNISCIENCE_FILTER_PROMPT = `You must strictly separate what the player can directly perceive from what they cannot. They should only read limited narrative content that their persona can actually see, hear, smell, touch, or otherwise directly sense. Before writing any narrative content that involves events, actions, or details the player directly cannot perceive (because they're not looking, too far away, behind them, in another room, happening silently, include NPCs' internal thoughts, etc.), you absolutely must output that hidden information inside a <filter> tag using this exact format:
|
||||
<filter event="[Brief description of what is happening that the player cannot perceive]" reason="[Why the player character cannot perceive this - e.g., 'behind them', 'in another room', 'too quiet to hear', 'focused elsewhere']"/>
|
||||
Example: <filter event="Zandik quietly takes the key from the table and slips out the back door" reason="Zandik is behind Mari, who is absorbed in reading, and he moves silently"/> You hear a faint click from somewhere behind you, but when you glance up from your newspaper, the room seems unchanged.`;
|
||||
|
||||
|
||||
/**
|
||||
* Default CYOA prompt text
|
||||
*/
|
||||
@@ -53,6 +62,11 @@ export const SPOTIFY_FORMAT_INSTRUCTION = `Include it in this exact format: <spo
|
||||
*/
|
||||
export const DEFAULT_NARRATOR_PROMPT = `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references; instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.`;
|
||||
|
||||
/**
|
||||
* Default Context Instructions prompt text (customizable by users)
|
||||
*/
|
||||
export const DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT = `The context above is information about the current scene, and what follows is the last message in the chat history. Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses.`;
|
||||
|
||||
/**
|
||||
* Gets character card information for current chat (handles both single and group chats)
|
||||
* @returns {string} Formatted character information
|
||||
|
||||
@@ -81,7 +81,7 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`);
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson.${item}`);
|
||||
return `
|
||||
<div class="rpg-item-card" data-field="onPerson" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
@@ -94,7 +94,7 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`);
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson.${item}`);
|
||||
return `
|
||||
<div class="rpg-item-row" data-field="onPerson" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
@@ -163,7 +163,7 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`);
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing.${item}`);
|
||||
return `
|
||||
<div class="rpg-item-card" data-field="clothing" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
@@ -176,7 +176,7 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`);
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing.${item}`);
|
||||
return `
|
||||
<div class="rpg-item-row" data-field="clothing" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
@@ -291,7 +291,7 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`);
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}.${item}`);
|
||||
return `
|
||||
<div class="rpg-item-card" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
@@ -304,7 +304,7 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`);
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}.${item}`);
|
||||
return `
|
||||
<div class="rpg-item-row" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
@@ -393,7 +393,7 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`);
|
||||
return `
|
||||
<div class="rpg-item-card" data-field="assets" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
@@ -406,7 +406,7 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
|
||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`);
|
||||
return `
|
||||
<div class="rpg-item-row" data-field="assets" data-index="${index}">
|
||||
${lockIconHtml}
|
||||
|
||||
@@ -212,8 +212,12 @@ export function renderQuests() {
|
||||
// Get current sub-tab from container or default to 'main'
|
||||
const activeSubTab = $questsContainer.data('active-subtab') || 'main';
|
||||
|
||||
// Get quests data
|
||||
const mainQuest = extensionSettings.quests.main || 'None';
|
||||
// Get quests data - extract value if it's a locked object
|
||||
let mainQuest = extensionSettings.quests.main || 'None';
|
||||
// Recursively extract value if it's nested objects
|
||||
while (typeof mainQuest === 'object' && mainQuest.value !== undefined) {
|
||||
mainQuest = mainQuest.value;
|
||||
}
|
||||
const optionalQuests = extensionSettings.quests.optional || [];
|
||||
|
||||
// Build HTML
|
||||
|
||||
@@ -50,9 +50,11 @@ function debugLog(message, data = null) {
|
||||
* @param {number} percentage - Value from 0-100
|
||||
* @param {string} lowColor - Hex color for low values (e.g., '#ff0000')
|
||||
* @param {string} highColor - Hex color for high values (e.g., '#00ff00')
|
||||
* @returns {string} Interpolated hex color
|
||||
* @param {number} lowOpacity - Opacity for low values (0-100)
|
||||
* @param {number} highOpacity - Opacity for high values (0-100)
|
||||
* @returns {string} Interpolated rgba color
|
||||
*/
|
||||
function getStatColor(percentage, lowColor, highColor) {
|
||||
function getStatColor(percentage, lowColor, highColor, lowOpacity = 100, highOpacity = 100) {
|
||||
// Clamp percentage to 0-100
|
||||
const percent = Math.max(0, Math.min(100, percentage)) / 100;
|
||||
|
||||
@@ -73,10 +75,9 @@ function getStatColor(percentage, lowColor, highColor) {
|
||||
const r = Math.round(low.r + (high.r - low.r) * percent);
|
||||
const g = Math.round(low.g + (high.g - low.g) * percent);
|
||||
const b = Math.round(low.b + (high.b - low.b) * percent);
|
||||
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
|
||||
|
||||
// Convert back to hex
|
||||
const toHex = (n) => n.toString(16).padStart(2, '0');
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,11 +153,20 @@ function namesMatch(cardName, aiName) {
|
||||
* Displays character cards with avatars, relationship badges, and traits.
|
||||
* Includes event listeners for editable character fields.
|
||||
*/
|
||||
export function renderThoughts() {
|
||||
export function renderThoughts({ preserveScroll = false } = {}) {
|
||||
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save scroll position before re-render if requested
|
||||
let savedContentScroll = 0;
|
||||
if (preserveScroll) {
|
||||
const $content = $thoughtsContainer.find('.rpg-thoughts-content');
|
||||
if ($content.length) {
|
||||
savedContentScroll = $content[0].scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't render if no data exists (e.g., after cache clear)
|
||||
const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts;
|
||||
if (!thoughtsData) {
|
||||
@@ -491,17 +501,19 @@ export function renderThoughts() {
|
||||
|
||||
html += `
|
||||
<div class="rpg-character-card" data-character-name="${char.name}">
|
||||
<div class="rpg-character-header-row">
|
||||
<div class="rpg-character-avatar rpg-avatar-upload" data-character="${char.name}" title="Click to upload avatar">
|
||||
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
||||
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${relationshipFieldName}" title="Click to edit (use emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
|
||||
</div>
|
||||
<div class="rpg-character-content">
|
||||
<div class="rpg-character-info">
|
||||
<div class="rpg-character-header">
|
||||
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="Click to edit emoji">${char.emoji}</span>
|
||||
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="Click to edit name">${char.name}</span>
|
||||
<button class="rpg-character-remove" data-character="${char.name}" title="Remove character">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-character-content">
|
||||
<div class="rpg-character-info">
|
||||
`;
|
||||
|
||||
// Render custom fields dynamically
|
||||
@@ -542,7 +554,13 @@ export function renderThoughts() {
|
||||
<div class="rpg-character-stats-inner">`;
|
||||
for (const stat of enabledCharStats) {
|
||||
const statValue = char[stat.name] || 0;
|
||||
const statColor = getStatColor(statValue, extensionSettings.statBarColorLow, extensionSettings.statBarColorHigh);
|
||||
const statColor = getStatColor(
|
||||
statValue,
|
||||
extensionSettings.statBarColorLow,
|
||||
extensionSettings.statBarColorHigh,
|
||||
extensionSettings.statBarColorLowOpacity ?? 100,
|
||||
extensionSettings.statBarColorHighOpacity ?? 100
|
||||
);
|
||||
html += `
|
||||
<div class="rpg-character-stat">
|
||||
<span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="Click to edit ${stat.name}">${statValue}%</span>
|
||||
@@ -705,6 +723,14 @@ export function renderThoughts() {
|
||||
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 600);
|
||||
}
|
||||
|
||||
// Restore scroll position after re-render
|
||||
if (preserveScroll) {
|
||||
const $content = $thoughtsContainer.find('.rpg-thoughts-content');
|
||||
if ($content.length) {
|
||||
$content[0].scrollTop = savedContentScroll;
|
||||
}
|
||||
}
|
||||
|
||||
// Update chat overlay if enabled
|
||||
if (extensionSettings.showThoughtsInChat) {
|
||||
updateChatThoughts();
|
||||
@@ -1053,11 +1079,25 @@ export function updateCharacterField(characterName, field, value) {
|
||||
// Check if it's a character stat
|
||||
const isStatField = enabledCharStats.findIndex(s => s.name === field) !== -1;
|
||||
if (isStatField) {
|
||||
if (!char.stats) char.stats = {};
|
||||
let numValue = parseInt(value.replace('%', '').trim());
|
||||
if (isNaN(numValue)) numValue = 0;
|
||||
numValue = Math.max(0, Math.min(100, numValue));
|
||||
|
||||
// Handle both array format (from LLM) and object format
|
||||
if (Array.isArray(char.stats)) {
|
||||
// Array format: [{name: "Health", value: 80}]
|
||||
const statIndex = char.stats.findIndex(s => s.name === field);
|
||||
if (statIndex !== -1) {
|
||||
char.stats[statIndex].value = numValue;
|
||||
} else {
|
||||
// Stat not found in array - add it
|
||||
char.stats.push({ name: field, value: numValue });
|
||||
}
|
||||
} else {
|
||||
// Object format: {Health: 80} or undefined
|
||||
if (!char.stats) char.stats = {};
|
||||
char.stats[field] = numValue;
|
||||
}
|
||||
} else {
|
||||
// It's a custom detail field - store in details object
|
||||
if (!char.details) char.details = {};
|
||||
@@ -1124,8 +1164,8 @@ export function updateCharacterField(characterName, field, value) {
|
||||
// console.log('[RPG Companion] JSON format updated successfully');
|
||||
// console.log('[RPG Companion] Updated data:', lastGeneratedData.characterThoughts);
|
||||
|
||||
// Re-render the thoughts panel to show updated value
|
||||
renderThoughts();
|
||||
// Re-render the thoughts panel to show updated value (preserve scroll position)
|
||||
renderThoughts({ preserveScroll: true });
|
||||
|
||||
// Update chat thought overlays if editing thoughts
|
||||
const thoughtsFieldName = presentCharsConfig?.thoughts?.name || 'Thoughts';
|
||||
|
||||
@@ -21,6 +21,21 @@ import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||
import { buildInventorySummary } from '../generation/promptBuilder.js';
|
||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||
import { updateFabWidgets } from '../ui/mobile.js';
|
||||
import { getStatBarColors } from '../ui/theme.js';
|
||||
|
||||
/**
|
||||
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
|
||||
* Example: "Conditions (up to 5 traits)" -> "conditions"
|
||||
* @param {string} name - Field name, possibly with parenthetical description
|
||||
* @returns {string} snake_case key from the base name only
|
||||
*/
|
||||
function toFieldKey(name) {
|
||||
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
||||
return baseName
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the user stats text string using custom stat names
|
||||
@@ -106,7 +121,7 @@ function updateUserStatsData() {
|
||||
// Then, add any other numeric stats from extensionSettings that aren't in config
|
||||
// (these could be custom stats the AI added or disabled stats)
|
||||
const customFields = config.statusSection?.customFields || [];
|
||||
const excludeFields = new Set(['mood', ...customFields.map(f => f.toLowerCase()), 'inventory', 'skills', 'level']);
|
||||
const excludeFields = new Set(['mood', ...customFields.map(f => toFieldKey(f)), 'inventory', 'skills', 'level']);
|
||||
Object.entries(stats).forEach(([key, value]) => {
|
||||
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
|
||||
statsArray.push({
|
||||
@@ -126,7 +141,7 @@ function updateUserStatsData() {
|
||||
|
||||
// Add all custom status fields
|
||||
for (const fieldName of customFields) {
|
||||
const fieldKey = fieldName.toLowerCase();
|
||||
const fieldKey = toFieldKey(fieldName);
|
||||
jsonData.status[fieldKey] = stats[fieldKey] || 'None';
|
||||
}
|
||||
|
||||
@@ -251,8 +266,9 @@ export function renderUserStats() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create gradient from low to high color
|
||||
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
|
||||
// Create gradient from low to high color with opacity
|
||||
const colors = getStatBarColors();
|
||||
const gradient = `linear-gradient(to right, ${colors.low}, ${colors.high})`;
|
||||
|
||||
// Check if stats bars section is locked
|
||||
const isStatsLocked = isItemLocked('userStats', 'stats');
|
||||
@@ -332,10 +348,13 @@ export function renderUserStats() {
|
||||
// Render custom status fields
|
||||
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
|
||||
for (const fieldName of config.statusSection.customFields) {
|
||||
const fieldKey = fieldName.toLowerCase();
|
||||
const fieldKey = toFieldKey(fieldName);
|
||||
let fieldValue = stats[fieldKey] || 'None';
|
||||
// Handle array format (from JSON)
|
||||
if (Array.isArray(fieldValue)) {
|
||||
fieldValue = fieldValue.join(', ') || 'None';
|
||||
} else if (typeof fieldValue === 'string') {
|
||||
// Strip brackets if present (from JSON array format)
|
||||
if (typeof fieldValue === 'string') {
|
||||
fieldValue = fieldValue.replace(/^\[|\]$/g, '').trim();
|
||||
}
|
||||
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="${fieldKey}" title="Click to edit ${fieldName}">${fieldValue}</div>`;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||
import { hexToRgba } from './theme.js';
|
||||
|
||||
/**
|
||||
* Helper to parse time string and calculate clock hand angles
|
||||
@@ -237,7 +238,9 @@ export function updateStripWidgets() {
|
||||
*/
|
||||
function getStatColor(value) {
|
||||
const lowColor = extensionSettings.statBarColorLow || '#cc3333';
|
||||
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
|
||||
const highColor = extensionSettings.statBarColorHigh || '#33cc66';
|
||||
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
|
||||
|
||||
// Simple linear interpolation between low and high colors
|
||||
const percent = Math.min(100, Math.max(0, value)) / 100;
|
||||
@@ -246,13 +249,14 @@ function getStatColor(value) {
|
||||
const lowRGB = hexToRgb(lowColor);
|
||||
const highRGB = hexToRgb(highColor);
|
||||
|
||||
if (!lowRGB || !highRGB) return value > 50 ? highColor : lowColor;
|
||||
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
|
||||
|
||||
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
|
||||
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
|
||||
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
|
||||
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
*/
|
||||
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import { generateRaw, chat, saveChatDebounced, characters, this_chid, user_avatar } from '../../../../../../../script.js';
|
||||
import { chat, saveChatDebounced, characters, this_chid, user_avatar } from '../../../../../../../script.js';
|
||||
import { safeGenerateRaw } from '../../utils/responseExtractor.js';
|
||||
import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js';
|
||||
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
||||
import { extensionSettings } from '../../core/state.js';
|
||||
@@ -81,7 +82,7 @@ export class EncounterModal {
|
||||
// Store request for potential regeneration
|
||||
this.lastRequest = { type: 'init', prompt: initPrompt };
|
||||
|
||||
const response = await generateRaw({
|
||||
const response = await safeGenerateRaw({
|
||||
prompt: initPrompt,
|
||||
quietToLoud: false
|
||||
});
|
||||
@@ -816,7 +817,7 @@ export class EncounterModal {
|
||||
// Store request for potential regeneration
|
||||
this.lastRequest = { type: 'action', action, prompt: actionPrompt };
|
||||
|
||||
const response = await generateRaw({
|
||||
const response = await safeGenerateRaw({
|
||||
prompt: actionPrompt,
|
||||
quietToLoud: false
|
||||
});
|
||||
@@ -1078,7 +1079,7 @@ export class EncounterModal {
|
||||
// Generate summary
|
||||
const summaryPrompt = await buildCombatSummaryPrompt(currentEncounter.encounterLog, result);
|
||||
|
||||
const summaryResponse = await generateRaw({
|
||||
const summaryResponse = await safeGenerateRaw({
|
||||
prompt: summaryPrompt,
|
||||
quietToLoud: false
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { saveSettings } from '../../core/persistence.js';
|
||||
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
|
||||
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
import { hexToRgba } from './theme.js';
|
||||
|
||||
/**
|
||||
* Updates the text labels of the mobile navigation tabs based on the current language.
|
||||
@@ -793,12 +794,17 @@ export function setupMobileKeyboardHandling() {
|
||||
/**
|
||||
* Handles focus on contenteditable fields to ensure they're visible when keyboard appears.
|
||||
* Uses smooth scrolling to bring focused field into view with proper padding.
|
||||
* Only applies on mobile viewports where virtual keyboard can obscure content.
|
||||
*/
|
||||
export function setupContentEditableScrolling() {
|
||||
const $panel = $('#rpg-companion-panel');
|
||||
|
||||
// Use event delegation for all contenteditable fields
|
||||
$panel.on('focusin', '[contenteditable="true"]', function(e) {
|
||||
// Only apply scrolling behavior on mobile (where virtual keyboard appears)
|
||||
const isMobile = window.innerWidth <= 1000;
|
||||
if (!isMobile) return;
|
||||
|
||||
const $field = $(this);
|
||||
|
||||
// Small delay to let keyboard animate in
|
||||
@@ -1567,7 +1573,9 @@ export function updateFabWidgets() {
|
||||
*/
|
||||
function getStatColor(value) {
|
||||
const lowColor = extensionSettings.statBarColorLow || '#cc3333';
|
||||
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
|
||||
const highColor = extensionSettings.statBarColorHigh || '#33cc66';
|
||||
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
|
||||
|
||||
// Simple linear interpolation between low and high colors
|
||||
const percent = Math.min(100, Math.max(0, value)) / 100;
|
||||
@@ -1576,13 +1584,14 @@ function getStatColor(value) {
|
||||
const lowRGB = hexToRgb(lowColor);
|
||||
const highRGB = hexToRgb(highColor);
|
||||
|
||||
if (!lowRGB || !highRGB) return value > 50 ? highColor : lowColor;
|
||||
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
|
||||
|
||||
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
|
||||
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
|
||||
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
|
||||
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { extensionSettings } from '../../core/state.js';
|
||||
import { saveSettings } from '../../core/persistence.js';
|
||||
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT } from '../generation/promptBuilder.js';
|
||||
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_OMNISCIENCE_FILTER_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT, DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT } from '../generation/promptBuilder.js';
|
||||
|
||||
let $editorModal = null;
|
||||
let tempPrompts = null; // Temporary prompts for cancel functionality
|
||||
@@ -14,9 +14,11 @@ const DEFAULT_PROMPTS = {
|
||||
html: DEFAULT_HTML_PROMPT,
|
||||
dialogueColoring: DEFAULT_DIALOGUE_COLORING_PROMPT,
|
||||
deception: DEFAULT_DECEPTION_PROMPT,
|
||||
omniscience: DEFAULT_OMNISCIENCE_FILTER_PROMPT,
|
||||
cyoa: DEFAULT_CYOA_PROMPT,
|
||||
spotify: DEFAULT_SPOTIFY_PROMPT,
|
||||
narrator: DEFAULT_NARRATOR_PROMPT,
|
||||
contextInstructions: DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
|
||||
plotRandom: 'Actually, the scene is getting stale. Introduce {{random::stakes::a plot twist::a new character::a cataclysm::a fourth-wall-breaking joke::a sudden atmospheric phenomenon::a plot hook::a running gag::an ecchi scenario::Death from Discworld::a new stake::a drama::a conflict::an angered entity::a god::a vision::a prophetic dream::Il Dottore from Genshin Impact::a new development::a civilian in need::an emotional bit::a threat::a villain::an important memory recollection::a marriage proposal::a date idea::an angry horde of villagers with pitchforks::a talking animal::an enemy::a cliffhanger::a short omniscient POV shift to a completely different character::a quest::an unexpected revelation::a scandal::an evil clone::death of an important character::harm to an important character::a romantic setup::a gossip::a messenger::a plot point from the past::a plot hole::a tragedy::a ghost::an otherworldly occurrence::a plot device::a curse::a magic device::a rival::an unexpected pregnancy::a brothel::a prostitute::a new location::a past lover::a completely random thing::a what-if scenario::a significant choice::war::love::a monster::lewd undertones::Professor Mari::a travelling troupe::a secret::a fortune-teller::something completely different::a killer::a murder mystery::a mystery::a skill check::a deus ex machina::three raccoons in a trench coat::a pet::a slave::an orphan::a psycho::tentacles::"there is only one bed" trope::accidental marriage::a fun twist::a boss battle::sexy corn::an eldritch horror::a character getting hungry, thirsty, or exhausted::horniness::a need for a bathroom break need::someone fainting::an assassination attempt::a meta narration of this all being an out of hand DND session::a dungeon::a friend in need::an old friend::a small time skip::a scene shift::Aurora Borealis, at this time of year, at this time of day, at this part of the country::a grand ball::a surprise party::zombies::foreshadowing::a Spanish Inquisition (nobody expects it)::a natural plot progression}} to make things more interesting! Be creative, but stay grounded in the setting.',
|
||||
plotNatural: 'Actually, the scene is getting stale. Progress it, to make things more interesting! Reintroduce an unresolved plot point from the past, or push the story further towards the current main goal. Be creative, but stay grounded in the setting.',
|
||||
avatar: `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons; however, your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable.
|
||||
@@ -96,9 +98,11 @@ function openPromptsEditor() {
|
||||
html: extensionSettings.customHtmlPrompt || '',
|
||||
dialogueColoring: extensionSettings.customDialogueColoringPrompt || '',
|
||||
deception: extensionSettings.customDeceptionPrompt || '',
|
||||
omniscience: extensionSettings.customOmnisciencePrompt || '',
|
||||
cyoa: extensionSettings.customCYOAPrompt || '',
|
||||
spotify: extensionSettings.customSpotifyPrompt || '',
|
||||
narrator: extensionSettings.customNarratorPrompt || '',
|
||||
contextInstructions: extensionSettings.customContextInstructionsPrompt || '',
|
||||
plotRandom: extensionSettings.customPlotRandomPrompt || '',
|
||||
plotNatural: extensionSettings.customPlotNaturalPrompt || '',
|
||||
avatar: extensionSettings.avatarLLMCustomInstruction || '',
|
||||
@@ -111,9 +115,11 @@ function openPromptsEditor() {
|
||||
$('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html);
|
||||
$('#rpg-prompt-dialogue-coloring').val(extensionSettings.customDialogueColoringPrompt || DEFAULT_PROMPTS.dialogueColoring);
|
||||
$('#rpg-prompt-deception').val(extensionSettings.customDeceptionPrompt || DEFAULT_PROMPTS.deception);
|
||||
$('#rpg-prompt-omniscience').val(extensionSettings.customOmnisciencePrompt || DEFAULT_PROMPTS.omniscience);
|
||||
$('#rpg-prompt-cyoa').val(extensionSettings.customCYOAPrompt || DEFAULT_PROMPTS.cyoa);
|
||||
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
|
||||
$('#rpg-prompt-narrator').val(extensionSettings.customNarratorPrompt || DEFAULT_PROMPTS.narrator);
|
||||
$('#rpg-prompt-context-instructions').val(extensionSettings.customContextInstructionsPrompt || DEFAULT_PROMPTS.contextInstructions);
|
||||
$('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom);
|
||||
$('#rpg-prompt-plot-natural').val(extensionSettings.customPlotNaturalPrompt || DEFAULT_PROMPTS.plotNatural);
|
||||
$('#rpg-prompt-avatar').val(extensionSettings.avatarLLMCustomInstruction || DEFAULT_PROMPTS.avatar);
|
||||
@@ -150,9 +156,11 @@ function savePrompts() {
|
||||
extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim();
|
||||
extensionSettings.customDialogueColoringPrompt = $('#rpg-prompt-dialogue-coloring').val().trim();
|
||||
extensionSettings.customDeceptionPrompt = $('#rpg-prompt-deception').val().trim();
|
||||
extensionSettings.customOmnisciencePrompt = $('#rpg-prompt-omniscience').val().trim();
|
||||
extensionSettings.customCYOAPrompt = $('#rpg-prompt-cyoa').val().trim();
|
||||
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim();
|
||||
extensionSettings.customNarratorPrompt = $('#rpg-prompt-narrator').val().trim();
|
||||
extensionSettings.customContextInstructionsPrompt = $('#rpg-prompt-context-instructions').val().trim();
|
||||
extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim();
|
||||
extensionSettings.customPlotNaturalPrompt = $('#rpg-prompt-plot-natural').val().trim();
|
||||
extensionSettings.avatarLLMCustomInstruction = $('#rpg-prompt-avatar').val().trim();
|
||||
@@ -182,6 +190,9 @@ function restorePromptToDefault(promptType) {
|
||||
case 'deception':
|
||||
extensionSettings.customDeceptionPrompt = '';
|
||||
break;
|
||||
case 'omniscience':
|
||||
extensionSettings.customOmnisciencePrompt = '';
|
||||
break;
|
||||
case 'cyoa':
|
||||
extensionSettings.customCYOAPrompt = '';
|
||||
break;
|
||||
@@ -191,6 +202,9 @@ function restorePromptToDefault(promptType) {
|
||||
case 'narrator':
|
||||
extensionSettings.customNarratorPrompt = '';
|
||||
break;
|
||||
case 'contextInstructions':
|
||||
extensionSettings.customContextInstructionsPrompt = '';
|
||||
break;
|
||||
case 'plotRandom':
|
||||
extensionSettings.customPlotRandomPrompt = '';
|
||||
break;
|
||||
@@ -221,9 +235,11 @@ function restoreAllToDefaults() {
|
||||
$('#rpg-prompt-html').val(DEFAULT_PROMPTS.html);
|
||||
$('#rpg-prompt-dialogue-coloring').val(DEFAULT_PROMPTS.dialogueColoring);
|
||||
$('#rpg-prompt-deception').val(DEFAULT_PROMPTS.deception);
|
||||
$('#rpg-prompt-omniscience').val(DEFAULT_PROMPTS.omniscience);
|
||||
$('#rpg-prompt-cyoa').val(DEFAULT_PROMPTS.cyoa);
|
||||
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
|
||||
$('#rpg-prompt-narrator').val(DEFAULT_PROMPTS.narrator);
|
||||
$('#rpg-prompt-context-instructions').val(DEFAULT_PROMPTS.contextInstructions);
|
||||
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
|
||||
$('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural);
|
||||
$('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar);
|
||||
@@ -235,9 +251,11 @@ function restoreAllToDefaults() {
|
||||
extensionSettings.customHtmlPrompt = '';
|
||||
extensionSettings.customDialogueColoringPrompt = '';
|
||||
extensionSettings.customDeceptionPrompt = '';
|
||||
extensionSettings.customOmnisciencePrompt = '';
|
||||
extensionSettings.customCYOAPrompt = '';
|
||||
extensionSettings.customSpotifyPrompt = '';
|
||||
extensionSettings.customNarratorPrompt = '';
|
||||
extensionSettings.customContextInstructionsPrompt = '';
|
||||
extensionSettings.customPlotRandomPrompt = '';
|
||||
extensionSettings.customPlotNaturalPrompt = '';
|
||||
extensionSettings.avatarLLMCustomInstruction = '';
|
||||
|
||||
+55
-12
@@ -5,6 +5,37 @@
|
||||
|
||||
import { extensionSettings, $panelContainer } from '../../core/state.js';
|
||||
|
||||
/**
|
||||
* Converts hex color and opacity percentage to rgba string
|
||||
* @param {string} hex - Hex color (e.g., '#ff0000')
|
||||
* @param {number} opacity - Opacity percentage (0-100)
|
||||
* @returns {string} - RGBA color string
|
||||
*/
|
||||
export function hexToRgba(hex, opacity = 100) {
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
const a = opacity / 100;
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets stat bar colors with opacity applied
|
||||
* @returns {{low: string, high: string}} RGBA color strings for stat bars
|
||||
*/
|
||||
export function getStatBarColors() {
|
||||
return {
|
||||
low: hexToRgba(
|
||||
extensionSettings.statBarColorLow || '#cc3333',
|
||||
extensionSettings.statBarColorLowOpacity ?? 100
|
||||
),
|
||||
high: hexToRgba(
|
||||
extensionSettings.statBarColorHigh || '#33cc66',
|
||||
extensionSettings.statBarColorHighOpacity ?? 100
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the selected theme to the panel.
|
||||
*/
|
||||
@@ -75,24 +106,33 @@ export function applyCustomTheme() {
|
||||
|
||||
const colors = extensionSettings.customColors;
|
||||
|
||||
// Convert hex colors with opacity to rgba
|
||||
const bgColor = hexToRgba(colors.bg, colors.bgOpacity ?? 100);
|
||||
const accentColor = hexToRgba(colors.accent, colors.accentOpacity ?? 100);
|
||||
const textColor = hexToRgba(colors.text, colors.textOpacity ?? 100);
|
||||
const highlightColor = hexToRgba(colors.highlight, colors.highlightOpacity ?? 100);
|
||||
|
||||
// Create shadow with 50% opacity of highlight color
|
||||
const shadowColor = hexToRgba(colors.highlight, (colors.highlightOpacity ?? 100) * 0.5);
|
||||
|
||||
// Apply custom CSS variables as inline styles to main panel
|
||||
$panelContainer.css({
|
||||
'--rpg-bg': colors.bg,
|
||||
'--rpg-accent': colors.accent,
|
||||
'--rpg-text': colors.text,
|
||||
'--rpg-highlight': colors.highlight,
|
||||
'--rpg-border': colors.highlight,
|
||||
'--rpg-shadow': `${colors.highlight}80` // Add alpha for shadow
|
||||
'--rpg-bg': bgColor,
|
||||
'--rpg-accent': accentColor,
|
||||
'--rpg-text': textColor,
|
||||
'--rpg-highlight': highlightColor,
|
||||
'--rpg-border': highlightColor,
|
||||
'--rpg-shadow': shadowColor
|
||||
});
|
||||
|
||||
// Apply custom colors to mobile toggle and thought elements
|
||||
const customStyles = {
|
||||
'--rpg-bg': colors.bg,
|
||||
'--rpg-accent': colors.accent,
|
||||
'--rpg-text': colors.text,
|
||||
'--rpg-highlight': colors.highlight,
|
||||
'--rpg-border': colors.highlight,
|
||||
'--rpg-shadow': `${colors.highlight}80`
|
||||
'--rpg-bg': bgColor,
|
||||
'--rpg-accent': accentColor,
|
||||
'--rpg-text': textColor,
|
||||
'--rpg-highlight': highlightColor,
|
||||
'--rpg-border': highlightColor,
|
||||
'--rpg-shadow': shadowColor
|
||||
};
|
||||
|
||||
const $mobileToggle = $('#rpg-mobile-toggle');
|
||||
@@ -139,6 +179,7 @@ export function updateFeatureTogglesVisibility() {
|
||||
const $htmlToggle = $('#rpg-html-toggle-wrapper');
|
||||
const $dialogueColoringToggle = $('#rpg-dialogue-coloring-toggle-wrapper');
|
||||
const $deceptionToggle = $('#rpg-deception-toggle-wrapper');
|
||||
const $omniscienceToggle = $('#rpg-omniscience-toggle-wrapper');
|
||||
const $cyoaToggle = $('#rpg-cyoa-toggle-wrapper');
|
||||
const $spotifyToggle = $('#rpg-spotify-toggle-wrapper');
|
||||
|
||||
@@ -150,6 +191,7 @@ export function updateFeatureTogglesVisibility() {
|
||||
$htmlToggle.toggle(extensionSettings.showHtmlToggle);
|
||||
$dialogueColoringToggle.toggle(extensionSettings.showDialogueColoringToggle);
|
||||
$deceptionToggle.toggle(extensionSettings.showDeceptionToggle ?? true);
|
||||
$omniscienceToggle.toggle(extensionSettings.showOmniscienceToggle ?? true);
|
||||
$cyoaToggle.toggle(extensionSettings.showCYOAToggle ?? true);
|
||||
$spotifyToggle.toggle(extensionSettings.showSpotifyToggle);
|
||||
|
||||
@@ -161,6 +203,7 @@ export function updateFeatureTogglesVisibility() {
|
||||
const anyVisible = extensionSettings.showHtmlToggle ||
|
||||
extensionSettings.showDialogueColoringToggle ||
|
||||
(extensionSettings.showDeceptionToggle ?? true) ||
|
||||
(extensionSettings.showOmniscienceToggle ?? true) ||
|
||||
(extensionSettings.showCYOAToggle ?? true) ||
|
||||
extensionSettings.showSpotifyToggle ||
|
||||
extensionSettings.showDynamicWeatherToggle ||
|
||||
|
||||
@@ -107,7 +107,8 @@ function getCurrentTime() {
|
||||
|
||||
// Patterns for specific weather conditions (order matters - combined effects first)
|
||||
// Grouped by languages for easy editing
|
||||
const WEATHER_PATTERNS_BY_LANGUAGE = {
|
||||
// EXPORTED: Used by jsonPromptHelpers.js to provide valid weather keywords to LLM
|
||||
export const WEATHER_PATTERNS_BY_LANGUAGE = {
|
||||
en: [
|
||||
{ id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind
|
||||
{ id: "storm", patterns: [ "storm", "thunder", "lightning" ] }, // Rain + Lightning
|
||||
@@ -130,6 +131,63 @@ const WEATHER_PATTERNS_BY_LANGUAGE = {
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid weather keywords for LLM prompt injection.
|
||||
* Returns weather patterns for specified language or all languages.
|
||||
* This ensures LLM generates responses that exactly match our expected patterns.
|
||||
*
|
||||
* @param {string} [language] - Language code (e.g., 'en', 'ru'). If not specified, returns all languages.
|
||||
* @returns {Object} Object with weather type IDs as keys and arrays of valid keywords as values
|
||||
* @example
|
||||
* // Returns: { blizzard: ["blizzard"], storm: ["storm", "thunder", "lightning"], ... }
|
||||
* getWeatherKeywordsForPrompt('en');
|
||||
*/
|
||||
export function getWeatherKeywordsForPrompt(language) {
|
||||
const result = {};
|
||||
|
||||
// Get patterns for specified language or merge all languages
|
||||
const languagesToProcess = language && WEATHER_PATTERNS_BY_LANGUAGE[language]
|
||||
? { [language]: WEATHER_PATTERNS_BY_LANGUAGE[language] }
|
||||
: WEATHER_PATTERNS_BY_LANGUAGE;
|
||||
|
||||
for (const [lang, patterns] of Object.entries(languagesToProcess)) {
|
||||
for (const { id, patterns: keywords } of patterns) {
|
||||
if (!result[id]) {
|
||||
result[id] = [];
|
||||
}
|
||||
// Add keywords, avoiding duplicates
|
||||
for (const keyword of keywords) {
|
||||
if (!result[id].includes(keyword)) {
|
||||
result[id].push(keyword);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get weather keywords as a formatted string for LLM instructions.
|
||||
* Provides a clear template showing valid weather forecast values.
|
||||
*
|
||||
* @param {string} [language] - Language code. If not specified, uses all available patterns.
|
||||
* @returns {string} Formatted string for prompt injection
|
||||
* @example
|
||||
* // Returns: 'Valid forecast values: "blizzard", "storm", "thunder", "lightning", "wind", ...'
|
||||
* getWeatherKeywordsAsPromptString('en');
|
||||
*/
|
||||
export function getWeatherKeywordsAsPromptString(language) {
|
||||
const keywords = getWeatherKeywordsForPrompt(language);
|
||||
const allKeywords = [];
|
||||
|
||||
for (const patterns of Object.values(keywords)) {
|
||||
allKeywords.push(...patterns);
|
||||
}
|
||||
|
||||
return `Valid forecast values (use one of these exactly): ${allKeywords.map(k => `"${k}"`).join(', ')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse weather text to determine effect type
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Response Extractor Utility
|
||||
*
|
||||
* Handles extraction of text content from various API response formats.
|
||||
* Fixes the "No message generated" error caused by Claude models with
|
||||
* extended thinking, where the API response `content` field is an array
|
||||
* of content blocks instead of a single string.
|
||||
*
|
||||
* Also provides a safe wrapper around SillyTavern's `generateRaw` that
|
||||
* intercepts the raw fetch response as a fallback.
|
||||
*/
|
||||
|
||||
import { generateRaw } from '../../../../../../../script.js';
|
||||
|
||||
/**
|
||||
* Extracts text from any API response shape (Anthropic content-block arrays,
|
||||
* OpenAI choices, plain strings, etc.).
|
||||
*
|
||||
* @param {*} response - The raw API response (string, array, or object)
|
||||
* @returns {string} The extracted text content
|
||||
*/
|
||||
export function extractTextFromResponse(response) {
|
||||
if (!response) return '';
|
||||
if (typeof response === 'string') return response;
|
||||
|
||||
// Response itself is an array of content blocks (Anthropic extended thinking)
|
||||
if (Array.isArray(response)) {
|
||||
const texts = response
|
||||
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
||||
.map(b => b.text);
|
||||
if (texts.length > 0) return texts.join('\n');
|
||||
|
||||
const strings = response.filter(item => typeof item === 'string');
|
||||
if (strings.length > 0) return strings.join('\n');
|
||||
|
||||
return JSON.stringify(response);
|
||||
}
|
||||
|
||||
// response.content (string or Anthropic content array)
|
||||
if (response.content !== undefined && response.content !== null) {
|
||||
if (typeof response.content === 'string') return response.content;
|
||||
if (Array.isArray(response.content)) {
|
||||
const texts = response.content
|
||||
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
||||
.map(b => b.text);
|
||||
if (texts.length > 0) return texts.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI choices format
|
||||
if (response.choices?.[0]?.message?.content) {
|
||||
const c = response.choices[0].message.content;
|
||||
if (typeof c === 'string') return c;
|
||||
if (Array.isArray(c)) {
|
||||
const texts = c
|
||||
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
||||
.map(b => b.text);
|
||||
if (texts.length > 0) return texts.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Other common fields
|
||||
if (typeof response.text === 'string') return response.text;
|
||||
if (typeof response.message === 'string') return response.message;
|
||||
if (response.message?.content && typeof response.message.content === 'string') {
|
||||
return response.message.content;
|
||||
}
|
||||
|
||||
return JSON.stringify(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe wrapper around SillyTavern's `generateRaw`.
|
||||
*
|
||||
* Temporarily intercepts `window.fetch` to capture the raw API response.
|
||||
* If `generateRaw` throws "No message generated" (e.g. because the first
|
||||
* content block from Claude extended thinking is empty), we extract the
|
||||
* real text from the captured raw data ourselves.
|
||||
*
|
||||
* @param {object} options - Options passed directly to `generateRaw`
|
||||
* @param {Array<{role: string, content: string}>} options.prompt - Message array
|
||||
* @param {boolean} [options.quietToLoud] - Whether to use quiet-to-loud mode
|
||||
* @returns {Promise<string>} The generated text
|
||||
*/
|
||||
export async function safeGenerateRaw(options) {
|
||||
let capturedRawData = null;
|
||||
const originalFetch = window.fetch;
|
||||
|
||||
window.fetch = async function (...args) {
|
||||
const response = await originalFetch.apply(this, args);
|
||||
try {
|
||||
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
|
||||
if (url.includes('/api/backends/chat-completions/generate') ||
|
||||
(url.includes('/api/backends/') && url.includes('/generate'))) {
|
||||
const clone = response.clone();
|
||||
capturedRawData = await clone.json();
|
||||
}
|
||||
} catch (e) {
|
||||
/* ignore clone/parse errors */
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await generateRaw(options);
|
||||
return result;
|
||||
} catch (genErr) {
|
||||
if (genErr.message?.includes('No message generated') && capturedRawData) {
|
||||
console.warn(
|
||||
'[RPG Companion] generateRaw failed (likely extended thinking). Extracting from raw API data.',
|
||||
);
|
||||
const extracted = extractTextFromResponse(capturedRawData);
|
||||
if (!extracted || !extracted.trim()) {
|
||||
throw new Error('Could not extract text from API response');
|
||||
}
|
||||
return extracted;
|
||||
}
|
||||
throw genErr; // Re-throw non-related errors
|
||||
} finally {
|
||||
window.fetch = originalFetch; // ALWAYS restore original fetch
|
||||
}
|
||||
}
|
||||
@@ -2118,8 +2118,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
/* Present Characters - Character Cards */
|
||||
.rpg-character-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: clamp(8px, 1vw, 12px);
|
||||
flex-direction: column;
|
||||
gap: clamp(6px, 0.8vh, 10px);
|
||||
padding: clamp(6px, 1vh, 8px);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: clamp(4px, 0.5vh, 6px);
|
||||
@@ -2157,6 +2157,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
border-color: var(--rpg-highlight);
|
||||
}
|
||||
|
||||
/* Header row with avatar and name */
|
||||
.rpg-character-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(8px, 1vw, 12px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Character avatar container with relationship badge */
|
||||
.rpg-character-avatar {
|
||||
position: relative;
|
||||
@@ -2164,8 +2172,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
}
|
||||
|
||||
.rpg-character-avatar img {
|
||||
width: clamp(35px, 6vh, 45px);
|
||||
height: clamp(35px, 6vh, 45px);
|
||||
width: clamp(30px, 5vh, 40px);
|
||||
height: clamp(30px, 5vh, 40px);
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--rpg-highlight);
|
||||
object-fit: cover;
|
||||
@@ -2232,13 +2240,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Character info section */
|
||||
/* Character info section - now takes full width below header row */
|
||||
.rpg-character-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
gap: clamp(3px, 0.5vh, 5px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -2271,13 +2278,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
background: var(--rpg-highlight);
|
||||
}
|
||||
|
||||
/* Character header with emoji and name */
|
||||
/* Character header with emoji and name - now inside header row */
|
||||
.rpg-character-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(4px, 0.5vw, 6px);
|
||||
flex-wrap: nowrap; /* Prevent wrapping */
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rpg-character-emoji {
|
||||
|
||||
+78
-11
@@ -139,6 +139,15 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Omniscience Filter Toggle -->
|
||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-omniscience-toggle-wrapper">
|
||||
<label class="rpg-toggle-label" title="Omniscience Filter">
|
||||
<input type="checkbox" id="rpg-toggle-omniscience">
|
||||
<i class="fa-solid fa-eye-slash"></i>
|
||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.omniscienceFilter">Omniscience Filter</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CYOA Toggle -->
|
||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-cyoa-toggle-wrapper">
|
||||
<label class="rpg-toggle-label" title="CYOA">
|
||||
@@ -241,29 +250,49 @@
|
||||
<div class="rpg-setting-row">
|
||||
<label for="rpg-custom-bg"
|
||||
data-i18n-key="template.settingsModal.themeOptions.custom.background">Background:</label>
|
||||
<input type="color" id="rpg-custom-bg" value="#1a1a2e" />
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<input type="color" id="rpg-custom-bg" value="#1a1a2e" style="width: 60px;" />
|
||||
<input type="range" id="rpg-custom-bg-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||
<span id="rpg-custom-bg-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-setting-row">
|
||||
<label for="rpg-custom-accent"
|
||||
data-i18n-key="template.settingsModal.themeOptions.custom.accent">Accent:</label>
|
||||
<input type="color" id="rpg-custom-accent" value="#16213e" />
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<input type="color" id="rpg-custom-accent" value="#16213e" style="width: 60px;" />
|
||||
<input type="range" id="rpg-custom-accent-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||
<span id="rpg-custom-accent-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-setting-row">
|
||||
<label for="rpg-custom-text"
|
||||
data-i18n-key="template.settingsModal.themeOptions.custom.text">Text:</label>
|
||||
<input type="color" id="rpg-custom-text" value="#eaeaea" />
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<input type="color" id="rpg-custom-text" value="#eaeaea" style="width: 60px;" />
|
||||
<input type="range" id="rpg-custom-text-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||
<span id="rpg-custom-text-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-setting-row">
|
||||
<label for="rpg-custom-highlight"
|
||||
data-i18n-key="template.settingsModal.themeOptions.custom.highlight">Highlight:</label>
|
||||
<input type="color" id="rpg-custom-highlight" value="#e94560" />
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<input type="color" id="rpg-custom-highlight" value="#e94560" style="width: 60px;" />
|
||||
<input type="range" id="rpg-custom-highlight-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||
<span id="rpg-custom-highlight-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rpg-setting-row">
|
||||
<label for="rpg-stat-bar-color-low" data-i18n-key="template.settingsModal.theme.statBarLow">Stat Bar
|
||||
Color (Low):</label>
|
||||
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" />
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" style="width: 60px;" />
|
||||
<input type="range" id="rpg-stat-bar-color-low-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||
<span id="rpg-stat-bar-color-low-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||
</div>
|
||||
<small data-i18n-key="template.settingsModal.theme.statBarLowNote">Color when stats are at
|
||||
0%.</small>
|
||||
</div>
|
||||
@@ -271,7 +300,11 @@
|
||||
<div class="rpg-setting-row">
|
||||
<label for="rpg-stat-bar-color-high" data-i18n-key="template.settingsModal.theme.statBarHigh">Stat
|
||||
Bar Color (High):</label>
|
||||
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" />
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" style="width: 60px;" />
|
||||
<input type="range" id="rpg-stat-bar-color-high-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||
<span id="rpg-stat-bar-color-high-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||
</div>
|
||||
<small data-i18n-key="template.settingsModal.theme.statBarHighNote">Color when stats are at
|
||||
100%.</small>
|
||||
</div>
|
||||
@@ -388,6 +421,15 @@
|
||||
Display a toggle button to enable/disable special formatting of lies and deceptions crafted by the model, allowing it to easily track whenever one was committed, without showing it to the user.
|
||||
</small>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-show-omniscience-toggle" />
|
||||
<span data-i18n-key="template.settingsModal.display.showOmniscienceToggle">Show Omniscience Filter</span>
|
||||
</label>
|
||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||
data-i18n-key="template.settingsModal.display.showOmniscienceToggleNote">
|
||||
Display a toggle button to enable/disable the omniscience filter, which instructs the AI to hide information the player character cannot perceive (events behind them, in other rooms, etc.) in special tags.
|
||||
</small>
|
||||
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="rpg-toggle-show-cyoa-toggle" />
|
||||
<span data-i18n-key="template.settingsModal.display.showCYOAToggle">Show CYOA</span>
|
||||
@@ -996,6 +1038,20 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Omniscience Filter Prompt -->
|
||||
<div class="rpg-prompt-editor-section">
|
||||
<label for="rpg-prompt-omniscience" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||
<i class="fa-solid fa-eye-slash"></i> Omniscience Filter Prompt
|
||||
</label>
|
||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
||||
Injected when "Enable Omniscience Filter" is enabled. Instructs AI to separate information the player character cannot perceive into hidden filter tags.
|
||||
</small>
|
||||
<textarea id="rpg-prompt-omniscience" class="rpg-prompt-textarea" rows="6"></textarea>
|
||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="omniscience" style="margin-top: 8px;">
|
||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- CYOA Prompt -->
|
||||
<div class="rpg-prompt-editor-section">
|
||||
<label for="rpg-prompt-cyoa" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||
@@ -1038,6 +1094,20 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Context Instructions Prompt -->
|
||||
<div class="rpg-prompt-editor-section">
|
||||
<label for="rpg-prompt-context-instructions" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||
<i class="fa-solid fa-comment-dots"></i> Context Instructions Prompt
|
||||
</label>
|
||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
||||
Injected in Separate/External mode after the context summary. Tells the AI how to use the context.
|
||||
</small>
|
||||
<textarea id="rpg-prompt-context-instructions" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="contextInstructions" style="margin-top: 8px;">
|
||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Random Plot Progression Prompt -->
|
||||
<div class="rpg-prompt-editor-section">
|
||||
<label for="rpg-prompt-plot-random" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||
@@ -1124,9 +1194,6 @@
|
||||
</div>
|
||||
|
||||
<footer class="rpg-settings-popup-footer">
|
||||
<button id="rpg-prompts-restore-all" class="rpg-btn-secondary" type="button">
|
||||
<i class="fa-solid fa-rotate-left"></i> Restore All To Default
|
||||
</button>
|
||||
<div class="rpg-footer-right">
|
||||
<button id="rpg-prompts-cancel" class="rpg-btn-secondary" type="button">Cancel</button>
|
||||
<button id="rpg-prompts-save" class="rpg-btn-primary" type="button">
|
||||
@@ -1192,9 +1259,9 @@
|
||||
For the extension to work properly, **it is not recommended to use any models below 20B, especially if they're old.** It works best with the SOTA models such as Deepseek, Claude, GPT, or Gemini.
|
||||
</p>
|
||||
|
||||
<h4 style="margin-top: 20px; margin-bottom: 10px;"><strong>Special thanks to all the other contributors for this project:</strong></h4>
|
||||
<h4 style="margin-top: 20px; margin-bottom: 10px;"><strong>Special thanks to all the contributors for this project:</strong></h4>
|
||||
<p style="margin-left: 20px; line-height: 1.6;">
|
||||
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude (???), IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
||||
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude (???), IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Olaroll.
|
||||
</p>
|
||||
|
||||
<div style="margin-top: 20px; text-align: center;">
|
||||
|
||||
Reference in New Issue
Block a user