Fix extension loading, enhance theming, add horizontal scrolling, improve emoji parsing, rename to Main Quests

This commit is contained in:
Spicy_Marinara
2025-10-26 22:31:21 +01:00
parent d68ddd601e
commit 141a3f4bec
17 changed files with 2888 additions and 437 deletions
+207 -6
View File
@@ -12,6 +12,44 @@ import {
} from '../../core/state.js';
import { saveChatData } from '../../core/persistence.js';
/**
* Helper to separate emoji from text in a string
* Handles cases where there's no comma or space after emoji
* @param {string} str - String potentially containing emoji followed by text
* @returns {{emoji: string, text: string}} Separated emoji and text
*/
function separateEmojiFromText(str) {
if (!str) return { emoji: '', text: '' };
str = str.trim();
// Regex to match emoji at the start (handles most emoji including compound ones)
const emojiRegex = /^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F910}-\u{1F96B}\u{1F980}-\u{1F9E0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]+/u;
const emojiMatch = str.match(emojiRegex);
if (emojiMatch) {
const emoji = emojiMatch[0];
let text = str.substring(emoji.length).trim();
// Remove leading comma or space if present
text = text.replace(/^[,\s]+/, '');
return { emoji, text };
}
// No emoji found - check if there's a comma separator anyway
const commaParts = str.split(',');
if (commaParts.length >= 2) {
return {
emoji: commaParts[0].trim(),
text: commaParts.slice(1).join(',').trim()
};
}
// No clear separation - return original as text
return { emoji: '', text: str };
}
/**
* Renders the info box as a visual dashboard with calendar, weather, temperature, clock, and map widgets.
* Includes event listeners for editable fields.
@@ -155,11 +193,24 @@ export function renderInfoBox() {
}
} else if (line.startsWith('Weather:')) {
if (!parsedFields.weather) {
// New text format: Weather: [Emoji], [Forecast]
// New text format: Weather: [Emoji], [Forecast] OR Weather: [Emoji][Forecast] (no separator - FIXED)
const weatherStr = line.replace('Weather:', '').trim();
const weatherParts = weatherStr.split(',').map(p => p.trim());
data.weatherEmoji = weatherParts[0] || '';
data.weatherForecast = weatherParts[1] || '';
const { emoji, text } = separateEmojiFromText(weatherStr);
if (emoji && text) {
data.weatherEmoji = emoji;
data.weatherForecast = text;
} else if (weatherStr.includes(',')) {
// Fallback to comma split if emoji detection failed
const weatherParts = weatherStr.split(',').map(p => p.trim());
data.weatherEmoji = weatherParts[0] || '';
data.weatherForecast = weatherParts[1] || '';
} else {
// No clear separation - assume it's all forecast text
data.weatherEmoji = '🌤️'; // Default emoji
data.weatherForecast = weatherStr;
}
parsedFields.weather = true;
}
} else {
@@ -217,8 +268,11 @@ export function renderInfoBox() {
// });
// Build visual dashboard HTML
// Wrap all content in a scrollable container
let html = '<div class="rpg-info-content">';
// Row 1: Date, Weather, Temperature, Time widgets
let html = '<div class="rpg-dashboard rpg-dashboard-row-1">';
html += '<div class="rpg-dashboard rpg-dashboard-row-1">';
// Calendar widget - always show (editable even if empty)
// Display abbreviated version but allow editing full value
@@ -301,6 +355,67 @@ export function renderInfoBox() {
</div>
`;
// Row 3: Recent Events widget (notebook style) - dynamically show 1-3 events
// Parse Recent Events from infoBox string
let recentEvents = [];
if (committedTrackerData.infoBox) {
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:'));
if (recentEventsLine) {
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
if (eventsString) {
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e);
}
}
}
const validEvents = recentEvents.filter(e => e && e.trim() && e !== 'Event 1' && e !== 'Event 2' && e !== 'Event 3');
// If no valid events, show at least one placeholder
if (validEvents.length === 0) {
validEvents.push('Click to add event');
}
html += `
<div class="rpg-dashboard rpg-dashboard-row-3">
<div class="rpg-dashboard-widget rpg-events-widget">
<div class="rpg-notebook-header">
<div class="rpg-notebook-ring"></div>
<div class="rpg-notebook-ring"></div>
<div class="rpg-notebook-ring"></div>
</div>
<div class="rpg-notebook-title">Recent Events</div>
<div class="rpg-notebook-lines">
`;
// Dynamically generate event lines (max 3)
for (let i = 0; i < Math.min(validEvents.length, 3); i++) {
html += `
<div class="rpg-notebook-line">
<span class="rpg-bullet">•</span>
<span class="rpg-event-text rpg-editable" contenteditable="true" data-field="event${i + 1}" title="Click to edit">${validEvents[i]}</span>
</div>
`;
}
// If we have less than 3 events, add empty placeholders with + icon
for (let i = validEvents.length; i < 3; i++) {
html += `
<div class="rpg-notebook-line rpg-event-add">
<span class="rpg-bullet">+</span>
<span class="rpg-event-text rpg-editable rpg-event-placeholder" contenteditable="true" data-field="event${i + 1}" title="Click to add event">Add event...</span>
</div>
`;
}
html += `
</div>
</div>
</div>
`;
// Close the scrollable content wrapper
html += '</div>';
$infoBoxContainer.html(html);
// Add event handlers for editable Info Box fields
@@ -320,7 +435,12 @@ export function renderInfoBox() {
}
}
updateInfoBoxField(field, value);
// Handle recent events separately
if (field === 'event1' || field === 'event2' || field === 'event3') {
updateRecentEvent(field, value);
} else {
updateInfoBoxField(field, value);
}
});
// For date fields, show full value on focus
@@ -610,3 +730,84 @@ export function updateInfoBoxField(field, value) {
renderInfoBox();
}
}
/**
* Update a recent event in the committed tracker data
* @param {string} field - event1, event2, or event3
* @param {string} value - New event text
*/
function updateRecentEvent(field, value) {
// Map field to index
const eventIndex = {
'event1': 0,
'event2': 1,
'event3': 2
}[field];
if (eventIndex !== undefined) {
// Parse current infoBox to get existing events
const lines = (committedTrackerData.infoBox || '').split('\n');
let recentEvents = [];
// Find existing Recent Events line
const recentEventsLine = lines.find(line => line.startsWith('Recent Events:'));
if (recentEventsLine) {
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
if (eventsString) {
recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e);
}
}
// Ensure array has enough slots
while (recentEvents.length <= eventIndex) {
recentEvents.push('');
}
// Update the specific event
recentEvents[eventIndex] = value;
// Filter out empty events and rebuild the line
const validEvents = recentEvents.filter(e => e && e.trim());
const newRecentEventsLine = validEvents.length > 0
? `Recent Events: ${validEvents.join(', ')}`
: '';
// Update infoBox with new Recent Events line
const updatedLines = lines.filter(line => !line.startsWith('Recent Events:'));
if (newRecentEventsLine) {
// Add Recent Events line at the end (before any empty lines)
let insertIndex = updatedLines.length;
for (let i = updatedLines.length - 1; i >= 0; i--) {
if (updatedLines[i].trim() !== '') {
insertIndex = i + 1;
break;
}
}
updatedLines.splice(insertIndex, 0, newRecentEventsLine);
}
committedTrackerData.infoBox = updatedLines.join('\n');
lastGeneratedData.infoBox = updatedLines.join('\n');
// Update the message's swipe data
const chat = getContext().chat;
if (chat && chat.length > 0) {
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (!message.is_user) {
if (message.extra && message.extra.rpg_companion_swipes) {
const swipeId = message.swipe_id || 0;
if (message.extra.rpg_companion_swipes[swipeId]) {
message.extra.rpg_companion_swipes[swipeId].infoBox = updatedLines.join('\n');
}
}
break;
}
}
}
saveChatData();
renderInfoBox();
console.log(`[RPG Companion] Updated recent event ${field}:`, value);
}
}
+306
View File
@@ -0,0 +1,306 @@
/**
* Quests Rendering Module
* Handles UI rendering for quests system (main and optional quests)
*/
import { extensionSettings, $questsContainer } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js';
/**
* HTML escape helper
* @param {string} text - Text to escape
* @returns {string} Escaped HTML
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Renders the quests sub-tab navigation (Main, Optional)
* @param {string} activeTab - Currently active sub-tab ('main', 'optional')
* @returns {string} HTML for sub-tab navigation
*/
export function renderQuestsSubTabs(activeTab = 'main') {
return `
<div class="rpg-quests-subtabs">
<button class="rpg-quests-subtab ${activeTab === 'main' ? 'active' : ''}" data-tab="main">
Main Quest
</button>
<button class="rpg-quests-subtab ${activeTab === 'optional' ? 'active' : ''}" data-tab="optional">
Optional Quests
</button>
</div>
`;
}
/**
* Renders the main quest view
* @param {string} mainQuest - Current main quest title
* @returns {string} HTML for main quest view
*/
export function renderMainQuestView(mainQuest) {
const questDisplay = (mainQuest && mainQuest !== 'None') ? mainQuest : '';
const hasQuest = questDisplay.length > 0;
return `
<div class="rpg-quest-section">
<div class="rpg-quest-header">
<h3 class="rpg-quest-section-title">Main Quests</h3>
${!hasQuest ? `<button class="rpg-add-quest-btn" data-action="add-quest" data-field="main" title="Add main quests">
<i class="fa-solid fa-plus"></i> Add Quest
</button>` : ''}
</div>
<div class="rpg-quest-content">
${hasQuest ? `
<div class="rpg-inline-form" id="rpg-edit-quest-form-main" style="display: none;">
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-main" value="${escapeHtml(questDisplay)}" />
<div class="rpg-inline-buttons">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-edit-quest" data-field="main">
<i class="fa-solid fa-times"></i> Cancel
</button>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-edit-quest" data-field="main">
<i class="fa-solid fa-check"></i> Save
</button>
</div>
</div>
<div class="rpg-quest-item" data-field="main">
<div class="rpg-quest-title">${escapeHtml(questDisplay)}</div>
<div class="rpg-quest-actions">
<button class="rpg-quest-edit" data-action="edit-quest" data-field="main" title="Edit quest">
<i class="fa-solid fa-edit"></i>
</button>
<button class="rpg-quest-remove" data-action="remove-quest" data-field="main" title="Complete/Remove quest">
<i class="fa-solid fa-check"></i>
</button>
</div>
</div>
` : `
<div class="rpg-inline-form" id="rpg-add-quest-form-main" style="display: none;">
<input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="Enter main quests title..." />
<div class="rpg-inline-actions">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="main">
<i class="fa-solid fa-times"></i> Cancel
</button>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="main">
<i class="fa-solid fa-check"></i> Add
</button>
</div>
</div>
<div class="rpg-quest-empty">No active main quests</div>
`}
</div>
<div class="rpg-quest-hint">
<i class="fa-solid fa-lightbulb"></i>
The main quests represent your primary objective in the story.
</div>
</div>
`;
}
/**
* Renders the optional quests view
* @param {string[]} optionalQuests - Array of optional quest titles
* @returns {string} HTML for optional quests view
*/
export function renderOptionalQuestsView(optionalQuests) {
const quests = optionalQuests.filter(q => q && q !== 'None');
let questsHtml = '';
if (quests.length === 0) {
questsHtml = '<div class="rpg-quest-empty">No active optional quests</div>';
} else {
questsHtml = quests.map((quest, index) => `
<div class="rpg-quest-item" data-field="optional" data-index="${index}">
<div class="rpg-quest-title rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" title="Click to edit">${escapeHtml(quest)}</div>
<div class="rpg-quest-actions">
<button class="rpg-quest-remove" data-action="remove-quest" data-field="optional" data-index="${index}" title="Complete/Remove quest">
<i class="fa-solid fa-check"></i>
</button>
</div>
</div>
`).join('');
}
return `
<div class="rpg-quest-section">
<div class="rpg-quest-header">
<h3 class="rpg-quest-section-title">Optional Quests</h3>
<button class="rpg-add-quest-btn" data-action="add-quest" data-field="optional" title="Add optional quest">
<i class="fa-solid fa-plus"></i> Add Quest
</button>
</div>
<div class="rpg-quest-content">
<div class="rpg-inline-form" id="rpg-add-quest-form-optional" style="display: none;">
<input type="text" class="rpg-inline-input" id="rpg-new-quest-optional" placeholder="Enter optional quest title..." />
<div class="rpg-inline-buttons">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="optional">
<i class="fa-solid fa-times"></i> Cancel
</button>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="optional">
<i class="fa-solid fa-check"></i> Add
</button>
</div>
</div>
<div class="rpg-quest-list">
${questsHtml}
</div>
<div class="rpg-quest-hint">
<i class="fa-solid fa-info-circle"></i>
Optional quests are side objectives that complement your main story.
</div>
</div>
</div>
`;
}
/**
* Main render function for quests
*/
export function renderQuests() {
if (!extensionSettings.showInventory || !$questsContainer) {
return;
}
// 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';
const optionalQuests = extensionSettings.quests.optional || [];
// Build HTML
let html = '<div class="rpg-quests-wrapper">';
html += renderQuestsSubTabs(activeSubTab);
// Render active sub-tab
html += '<div class="rpg-quests-panels">';
if (activeSubTab === 'main') {
html += renderMainQuestView(mainQuest);
} else {
html += renderOptionalQuestsView(optionalQuests);
}
html += '</div></div>';
$questsContainer.html(html);
// Attach event handlers
attachQuestEventHandlers();
}
/**
* Attach event handlers for quest interactions
*/
function attachQuestEventHandlers() {
// Sub-tab switching
$questsContainer.find('.rpg-quests-subtab').on('click', function() {
const tab = $(this).data('tab');
$questsContainer.data('active-subtab', tab);
renderQuests();
});
// Add quest button
$questsContainer.find('[data-action="add-quest"]').on('click', function() {
const field = $(this).data('field');
$(`#rpg-add-quest-form-${field}`).show();
$(`#rpg-new-quest-${field}`).focus();
});
// Cancel add quest
$questsContainer.find('[data-action="cancel-add-quest"]').on('click', function() {
const field = $(this).data('field');
$(`#rpg-add-quest-form-${field}`).hide();
$(`#rpg-new-quest-${field}`).val('');
});
// Save add quest
$questsContainer.find('[data-action="save-add-quest"]').on('click', function() {
const field = $(this).data('field');
const input = $(`#rpg-new-quest-${field}`);
const questTitle = input.val().trim();
if (questTitle) {
if (field === 'main') {
extensionSettings.quests.main = questTitle;
} else {
if (!extensionSettings.quests.optional) {
extensionSettings.quests.optional = [];
}
extensionSettings.quests.optional.push(questTitle);
}
saveSettings();
renderQuests();
}
});
// Edit quest (main only)
$questsContainer.find('[data-action="edit-quest"]').on('click', function() {
const field = $(this).data('field');
$(`#rpg-edit-quest-form-${field}`).show();
$('.rpg-quest-item[data-field="main"]').hide();
$(`#rpg-edit-quest-${field}`).focus();
});
// Cancel edit quest
$questsContainer.find('[data-action="cancel-edit-quest"]').on('click', function() {
const field = $(this).data('field');
$(`#rpg-edit-quest-form-${field}`).hide();
$('.rpg-quest-item[data-field="main"]').show();
});
// Save edit quest
$questsContainer.find('[data-action="save-edit-quest"]').on('click', function() {
const field = $(this).data('field');
const input = $(`#rpg-edit-quest-${field}`);
const questTitle = input.val().trim();
if (questTitle) {
extensionSettings.quests.main = questTitle;
saveSettings();
renderQuests();
}
});
// Remove quest
$questsContainer.find('[data-action="remove-quest"]').on('click', function() {
const field = $(this).data('field');
const index = $(this).data('index');
if (field === 'main') {
extensionSettings.quests.main = 'None';
} else {
extensionSettings.quests.optional.splice(index, 1);
}
saveSettings();
renderQuests();
});
// Inline editing for optional quests
$questsContainer.find('.rpg-quest-title.rpg-editable').on('blur', function() {
const $this = $(this);
const field = $this.data('field');
const index = $this.data('index');
const newTitle = $this.text().trim();
if (newTitle && field === 'optional' && index !== undefined) {
extensionSettings.quests.optional[index] = newTitle;
saveSettings();
}
});
// Enter key to save in forms
$questsContainer.find('.rpg-inline-input').on('keypress', function(e) {
if (e.which === 13) {
const field = $(this).attr('id').includes('edit') ?
$(this).attr('id').replace('rpg-edit-quest-', '') :
$(this).attr('id').replace('rpg-new-quest-', '');
if ($(this).attr('id').includes('edit')) {
$(`[data-action="save-edit-quest"][data-field="${field}"]`).click();
} else {
$(`[data-action="save-add-quest"][data-field="${field}"]`).click();
}
}
});
}