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
|
## 🆕 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
@@ -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
@@ -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
@@ -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:",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user