v3.7.1: Weather keywords, character stat editing fix, scroll bug fix, avatar layout

- Improved weather generation: Added hard templates for weather keywords to ensure LLM generates valid weather patterns that match dynamic effects
- Fixed character stat editing bug: Now properly handles array format stats from LLM (values no longer revert on blur)
- Fixed scroll/viewport bug: Mobile-only scrollIntoView prevents page jumping on desktop when editing fields
- Changed Present Characters avatar display: Avatar now aligned with name in header row, fields take full width below
- Updated descriptions and labels
This commit is contained in:
Spicy_Marinara
2026-02-01 14:42:00 +01:00
parent b61a426efe
commit 32c4f67822
9 changed files with 123 additions and 32 deletions
+6 -7
View File
@@ -7,14 +7,13 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
## 🆕 What's New ## 🆕 What's New
### v3.7.0 ### v3.7.1
- Added omniscience filter. - Improved instructions for the model to generate dynamic weather.
- Added new prompts available for customization. - Small fixes and updates to descriptions.
- Added opacity to the color selector. - Fixed a scroll/viewport bug that moved everything up after you edited fields.
- Overwritten SillyTavern's dumb-ahh trim logic when joining prompts. - Changed the display of avatars for present characters.
- Fixed custom attributes not allowing value increase/decrease. -
- Various bug fixes.
**Special thanks to all the other contributors for this project:** **Special thanks to all the other contributors for this project:**
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein! Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein!
+1 -1
View File
@@ -6,6 +6,6 @@
"js": "index.js", "js": "index.js",
"css": "style.css", "css": "style.css",
"author": "Marinara", "author": "Marinara",
"version": "3.7.0", "version": "3.7.1",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
} }
+1 -1
View File
@@ -49,7 +49,7 @@
</div> </div>
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;"> <div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
v3.7.0 v3.7.1
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -159,7 +159,7 @@
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:", "template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:",
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats", "template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats",
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track 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.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat",
"template.mainPanel.title": "RPG Companion", "template.mainPanel.title": "RPG Companion",
"template.mainPanel.lastRoll": "Last Roll:", "template.mainPanel.lastRoll": "Last Roll:",
+6 -1
View File
@@ -5,6 +5,8 @@
import { extensionSettings, committedTrackerData } from '../../core/state.js'; import { extensionSettings, committedTrackerData } from '../../core/state.js';
import { getContext } from '../../../../../../extensions.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 * Converts a field name to snake_case for use as JSON key
@@ -132,7 +134,10 @@ export function buildInfoBoxJSONInstruction() {
} }
if (widgets.weather?.enabled) { 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; hasFields = true;
} }
+19 -3
View File
@@ -492,17 +492,19 @@ export function renderThoughts() {
html += ` html += `
<div class="rpg-character-card" data-character-name="${char.name}"> <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"> <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;" /> <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>` : ''} ${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>
<div class="rpg-character-content">
<div class="rpg-character-info">
<div class="rpg-character-header"> <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-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> <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> <button class="rpg-character-remove" data-character="${char.name}" title="Remove character">×</button>
</div> </div>
</div>
<div class="rpg-character-content">
<div class="rpg-character-info">
`; `;
// Render custom fields dynamically // Render custom fields dynamically
@@ -1060,11 +1062,25 @@ export function updateCharacterField(characterName, field, value) {
// Check if it's a character stat // Check if it's a character stat
const isStatField = enabledCharStats.findIndex(s => s.name === field) !== -1; const isStatField = enabledCharStats.findIndex(s => s.name === field) !== -1;
if (isStatField) { if (isStatField) {
if (!char.stats) char.stats = {};
let numValue = parseInt(value.replace('%', '').trim()); let numValue = parseInt(value.replace('%', '').trim());
if (isNaN(numValue)) numValue = 0; if (isNaN(numValue)) numValue = 0;
numValue = Math.max(0, Math.min(100, numValue)); 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; char.stats[field] = numValue;
}
} else { } else {
// It's a custom detail field - store in details object // It's a custom detail field - store in details object
if (!char.details) char.details = {}; if (!char.details) char.details = {};
+5
View File
@@ -794,12 +794,17 @@ export function setupMobileKeyboardHandling() {
/** /**
* Handles focus on contenteditable fields to ensure they're visible when keyboard appears. * 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. * 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() { export function setupContentEditableScrolling() {
const $panel = $('#rpg-companion-panel'); const $panel = $('#rpg-companion-panel');
// Use event delegation for all contenteditable fields // Use event delegation for all contenteditable fields
$panel.on('focusin', '[contenteditable="true"]', function(e) { $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); const $field = $(this);
// Small delay to let keyboard animate in // Small delay to let keyboard animate in
+59 -1
View File
@@ -107,7 +107,8 @@ function getCurrentTime() {
// Patterns for specific weather conditions (order matters - combined effects first) // Patterns for specific weather conditions (order matters - combined effects first)
// Grouped by languages for easy editing // 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: [ en: [
{ id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind { id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind
{ id: "storm", patterns: [ "storm", "thunder", "lightning" ] }, // Rain + Lightning { 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 * Parse weather text to determine effect type
*/ */
+18 -10
View File
@@ -2118,8 +2118,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
/* Present Characters - Character Cards */ /* Present Characters - Character Cards */
.rpg-character-card { .rpg-character-card {
display: flex; display: flex;
align-items: flex-start; flex-direction: column;
gap: clamp(8px, 1vw, 12px); gap: clamp(6px, 0.8vh, 10px);
padding: clamp(6px, 1vh, 8px); padding: clamp(6px, 1vh, 8px);
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
border-radius: clamp(4px, 0.5vh, 6px); border-radius: clamp(4px, 0.5vh, 6px);
@@ -2157,6 +2157,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
border-color: var(--rpg-highlight); 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 */ /* Character avatar container with relationship badge */
.rpg-character-avatar { .rpg-character-avatar {
position: relative; position: relative;
@@ -2164,8 +2172,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
} }
.rpg-character-avatar img { .rpg-character-avatar img {
width: clamp(35px, 6vh, 45px); width: clamp(30px, 5vh, 40px);
height: clamp(35px, 6vh, 45px); height: clamp(30px, 5vh, 40px);
border-radius: 50%; border-radius: 50%;
border: 2px solid var(--rpg-highlight); border: 2px solid var(--rpg-highlight);
object-fit: cover; 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); 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 { .rpg-character-content {
flex: 1; width: 100%;
min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; gap: clamp(3px, 0.5vh, 5px);
overflow: hidden; overflow: hidden;
} }
@@ -2271,13 +2278,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
background: var(--rpg-highlight); background: var(--rpg-highlight);
} }
/* Character header with emoji and name */ /* Character header with emoji and name - now inside header row */
.rpg-character-header { .rpg-character-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: clamp(4px, 0.5vw, 6px); gap: clamp(4px, 0.5vw, 6px);
flex-wrap: nowrap; /* Prevent wrapping */ flex-wrap: nowrap; /* Prevent wrapping */
position: relative; flex: 1;
min-width: 0;
} }
.rpg-character-emoji { .rpg-character-emoji {