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:
@@ -7,14 +7,13 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
|
||||
|
||||
## 🆕 What's New
|
||||
|
||||
### v3.7.0
|
||||
### v3.7.1
|
||||
|
||||
- Added omniscience filter.
|
||||
- Added new prompts available for customization.
|
||||
- Added opacity to the color selector.
|
||||
- Overwritten SillyTavern's dumb-ahh trim logic when joining prompts.
|
||||
- Fixed custom attributes not allowing value increase/decrease.
|
||||
- Various bug fixes.
|
||||
- Improved instructions for the model to generate dynamic weather.
|
||||
- Small fixes and updates to descriptions.
|
||||
- Fixed a scroll/viewport bug that moved everything up after you edited fields.
|
||||
- Changed the display of avatars for present characters.
|
||||
-
|
||||
|
||||
**Special thanks to all the other contributors for this project:**
|
||||
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein!
|
||||
|
||||
+1
-1
@@ -6,6 +6,6 @@
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Marinara",
|
||||
"version": "3.7.0",
|
||||
"version": "3.7.1",
|
||||
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
|
||||
v3.7.0
|
||||
v3.7.1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -159,7 +159,7 @@
|
||||
"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:",
|
||||
|
||||
@@ -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
|
||||
@@ -132,7 +134,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -492,17 +492,19 @@ export function renderThoughts() {
|
||||
|
||||
html += `
|
||||
<div class="rpg-character-card" data-character-name="${char.name}">
|
||||
<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 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-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">
|
||||
<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>
|
||||
`;
|
||||
|
||||
// Render custom fields dynamically
|
||||
@@ -1060,11 +1062,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));
|
||||
char.stats[field] = 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 = {};
|
||||
|
||||
@@ -794,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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user