Add holiday promotion, snowflakes effect, and Spotify music widget

- Added holiday promotion banner with 2026WITHMARINARA discount code
- Added dismiss functionality for promotion with persistent state
- Implemented snowflakes animation effect with toggle
- Added Spotify music widget above chat input
- Widget matches extension theme colors and positioning
- Added Display Options toggles to show/hide feature toggles
- Improved responsive design and mobile support
This commit is contained in:
Spicy_Marinara
2025-12-30 20:56:38 +01:00
parent 51535c5fdc
commit 3f58c7ceca
17 changed files with 1170 additions and 199 deletions
+67
View File
@@ -21,6 +21,7 @@ import {
$thoughtsContainer,
$inventoryContainer,
$questsContainer,
$musicPlayerContainer,
setExtensionSettings,
updateExtensionSettings,
setLastGeneratedData,
@@ -37,6 +38,7 @@ import {
setThoughtsContainer,
setInventoryContainer,
setQuestsContainer,
setMusicPlayerContainer,
clearSessionAvatarPrompts
} from './src/core/state.js';
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js';
@@ -66,6 +68,8 @@ import {
} from './src/systems/rendering/thoughts.js';
import { renderInventory } from './src/systems/rendering/inventory.js';
import { renderQuests } from './src/systems/rendering/quests.js';
import { renderMusicPlayer } from './src/systems/rendering/musicPlayer.js';
import { toggleSnowflakes, initSnowflakes } from './src/systems/ui/snowflakes.js';
// Interaction modules
import { initInventoryEventListeners } from './src/systems/interaction/inventoryActions.js';
@@ -76,6 +80,7 @@ import {
applyCustomTheme,
toggleCustomColors,
toggleAnimations,
updateFeatureTogglesVisibility,
updateSettingsPopupTheme,
applyCustomThemeToSettingsPopup
} from './src/systems/ui/theme.js';
@@ -130,6 +135,7 @@ import { setupClassicStatsButtons } from './src/systems/features/classicStats.js
import { ensureHtmlCleaningRegex, detectConflictingRegexScripts } from './src/systems/features/htmlCleaning.js';
import { setupMemoryRecollectionButton, updateMemoryRecollectionButton } from './src/systems/features/memoryRecollection.js';
import { initLorebookLimiter } from './src/systems/features/lorebookLimiter.js';
import { parseAndStoreSpotifyUrl } from './src/systems/features/musicPlayer.js';
import { DEFAULT_HTML_PROMPT } from './src/systems/generation/promptBuilder.js';
import { openEncounterModal } from './src/systems/ui/encounterUI.js';
@@ -277,6 +283,7 @@ async function initUI() {
setThoughtsContainer($('#rpg-thoughts'));
setInventoryContainer($('#rpg-inventory'));
setQuestsContainer($('#rpg-quests'));
setMusicPlayerContainer($('#rpg-music-player'));
// Re-apply translations to the entire body to catch all new elements from the template
i18n.applyTranslations(document.body);
@@ -377,6 +384,25 @@ async function initUI() {
saveSettings();
});
$('#rpg-toggle-spotify-music').on('change', function() {
extensionSettings.enableSpotifyMusic = $(this).prop('checked');
saveSettings();
updateSectionVisibility();
renderMusicPlayer($musicPlayerContainer[0]);
});
$('#rpg-toggle-snowflakes').on('change', function() {
extensionSettings.enableSnowflakes = $(this).prop('checked');
saveSettings();
toggleSnowflakes(extensionSettings.enableSnowflakes);
});
$('#rpg-dismiss-promo').on('click', function() {
extensionSettings.dismissedHolidayPromo = true;
saveSettings();
$('#rpg-holiday-promo').fadeOut(300);
});
$('#rpg-skip-guided-mode').on('change', function() {
extensionSettings.skipInjectionsForGuided = String($(this).val());
saveSettings();
@@ -516,6 +542,25 @@ async function initUI() {
toggleAnimations();
});
// Feature toggle visibility controls
$('#rpg-toggle-show-html-toggle').on('change', function() {
extensionSettings.showHtmlToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-spotify-toggle').on('change', function() {
extensionSettings.showSpotifyToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
$('#rpg-toggle-show-snowflakes-toggle').on('change', function() {
extensionSettings.showSnowflakesToggle = $(this).prop('checked');
saveSettings();
updateFeatureTogglesVisibility();
});
// Auto avatar generation settings
$('#rpg-toggle-auto-avatars').on('change', function() {
extensionSettings.autoGenerateAvatars = $(this).prop('checked');
@@ -710,6 +755,18 @@ async function initUI() {
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
$('#rpg-toggle-always-show-bubble').prop('checked', extensionSettings.alwaysShowThoughtBubble);
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
$('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic);
$('#rpg-toggle-snowflakes').prop('checked', extensionSettings.enableSnowflakes);
// Feature toggle visibility settings
$('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true);
$('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true);
$('#rpg-toggle-show-snowflakes-toggle').prop('checked', extensionSettings.showSnowflakesToggle ?? true);
// Hide holiday promo if previously dismissed
if (extensionSettings.dismissedHolidayPromo) {
$('#rpg-holiday-promo').hide();
}
$('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons);
$('#rpg-toggle-encounters').prop('checked', extensionSettings.encounterSettings?.enabled ?? true);
@@ -766,6 +823,7 @@ async function initUI() {
applyPanelPosition();
toggleCustomColors();
toggleAnimations();
updateFeatureTogglesVisibility();
// Setup mobile toggle button
setupMobileToggle();
@@ -784,6 +842,7 @@ async function initUI() {
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
updateDiceDisplay();
setupDiceRoller();
setupClassicStatsButtons();
@@ -983,6 +1042,14 @@ jQuery(async () => {
// Restore checkpoint state if one exists
await restoreCheckpointOnLoad();
// Initialize snowflakes effect if enabled
try {
initSnowflakes();
} catch (error) {
console.error('[RPG Companion] Snowflakes initialization failed:', error);
// Non-critical - continue without it
}
console.log('[RPG Companion] ✅ Extension loaded successfully');
} catch (error) {
console.error('[RPG Companion] ❌ Critical initialization failure:', error);
+2
View File
@@ -35,6 +35,8 @@ export const defaultSettings = {
showThoughtsInChat: true, // Show thoughts overlay in chat
alwaysShowThoughtBubble: false, // Auto-expand thought bubble without clicking icon
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
// Controls when the extension skips injecting tracker instructions/examples/HTML
// into generations that appear to be user-injected instructions. Valid values:
// - 'none' -> never skip (legacy behavior: always inject)
+12
View File
@@ -24,6 +24,13 @@ export let extensionSettings = {
narratorMode: false, // Use character card as narrator instead of fixed character references
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
enableSnowflakes: false, // Enable festive snowflakes effect
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
showSnowflakesToggle: true, // Show Snowflakes Effect toggle in main panel
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
enablePlotButtons: true, // Show plot progression buttons above chat input
saveTrackerHistory: false, // Save tracker data in chat history for each message
@@ -283,6 +290,7 @@ export let $infoBoxContainer = null;
export let $thoughtsContainer = null;
export let $inventoryContainer = null;
export let $questsContainer = null;
export let $musicPlayerContainer = null;
/**
* State setters - provide controlled mutation of state variables
@@ -354,3 +362,7 @@ export function setInventoryContainer($element) {
export function setQuestsContainer($element) {
$questsContainer = $element;
}
export function setMusicPlayerContainer($element) {
$musicPlayerContainer = $element;
}
+9 -2
View File
@@ -39,6 +39,9 @@
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first",
"template.settingsModal.display.enableAnimations": "Enable Animations",
"template.settingsModal.display.enableAnimationsNote": "Smooth transitions for stats, content updates, and dice rolls",
"template.settingsModal.display.showImmersiveHtmlToggle": "Show Immersive HTML",
"template.settingsModal.display.showSpotifyMusicToggle": "Show Spotify Music",
"template.settingsModal.display.showSnowflakesToggle": "Show Snowflakes Effect",
"template.settingsModal.display.showPlotProgressionButtons": "Show Plot Progression Buttons",
"template.settingsModal.display.showPlotProgressionButtonsNote": "Display buttons above chat input for plot progression prompts",
"template.settingsModal.display.showDiceDisplay": "Show Dice Roll Display",
@@ -126,7 +129,9 @@
"template.mainPanel.title": "RPG Companion",
"template.mainPanel.lastRoll": "Last Roll:",
"template.mainPanel.clearLastRoll": "Clear last roll",
"template.mainPanel.enableImmersiveHtml": "Enable Immersive HTML",
"template.mainPanel.immersiveHtml": "Immersive HTML",
"template.mainPanel.spotifyMusic": "Spotify Music",
"template.mainPanel.snowflakesEffect": "Snowflakes Effect",
"template.mainPanel.refreshRpgInfo": "Refresh RPG Info",
"template.mainPanel.updating": "Updating...",
"template.mainPanel.editTrackersButton": "Edit Trackers",
@@ -183,5 +188,7 @@
"checkpoint.setChapterStart": "Set Chapter Start",
"checkpoint.clearChapterStart": "Clear Chapter Start",
"checkpoint.indicator": "Chapter Start",
"checkpoint.tooltip": "Messages before this point are excluded from context"
"checkpoint.tooltip": "Messages before this point are excluded from context",
"musicPlayer.title": "Scene Music",
"musicPlayer.noMusic": "AI will suggest music when appropriate for the scene"
}
+170 -163
View File
@@ -1,49 +1,52 @@
{
"settings.language.label": "語言",
"settings.language.option.en": "English",
"settings.language.option.zh-tw": "繁體中文",
"settings.extensionEnabled": "啟用 RPG Companion",
"settings.note": "切換開關以啟用/停用 RPG Companion。其他設定可在面板內配置。",
"template.settingsTitle": "RPG Companion 設定",
"template.settingsModal.themeTitle": "主題",
"template.settingsModal.themeLabel": "可選主題:",
"template.settingsModal.themeOptions.default": "預設",
"template.settingsModal.themeOptions.sciFi": "科幻 (合成波)",
"template.settingsModal.themeOptions.fantasy": "奇幻 (古樸羊皮紙)",
"template.settingsModal.themeOptions.cyberpunk": "賽博朋克 (霓虹網格)",
"template.settingsModal.themeOptions.custom": "自訂",
"template.settingsModal.themeOptions.custom.background": "背景:",
"template.settingsModal.themeOptions.custom.accent": "強調色:",
"template.settingsModal.themeOptions.custom.text": "文字:",
"template.settingsModal.themeOptions.custom.highlight": "高亮:",
"template.settingsModal.theme.statBarLow": "屬性條顏色 (低)",
"template.settingsModal.theme.statBarLowNote": "屬性在 0% 時的顏色",
"template.settingsModal.theme.statBarHigh": "屬性條顏色 (高)",
"template.settingsModal.theme.statBarHighNote": "屬性在 100% 時的顏色",
"template.settingsModal.displayTitle": "顯示設定",
"template.settingsModal.displayNote": "使用擴充功能標籤來啟用/停用 RPG Companion 擴充功能。",
"template.settingsModal.display.panelPosition": "面板位置:",
"template.settingsModal.display.panelPositionOptions.right": "右側邊欄",
"template.settingsModal.display.panelPositionOptions.left": "左側邊欄",
"template.settingsModal.display.toggleAutoUpdate": "訊息後自動更新",
"template.settingsModal.display.showUserStats": "顯示 user 屬性",
"template.settingsModal.display.showInfoBox": "顯示資訊框",
"template.settingsModal.display.showPresentCharacters": "顯示在場角色",
"template.settingsModal.display.showInventory": "顯示物品欄",
"template.settingsModal.display.showThoughtsInChat": "在聊天中顯示想法",
"template.settingsModal.display.showThoughtsInChatNote": "將角色想法顯示為其訊息旁的泡泡",
"template.settingsModal.display.alwaysShowThoughtBubble": "始終顯示想法泡泡",
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自動展開想法泡泡",
"template.settingsModal.display.enableAnimations": "啟用動畫",
"template.settingsModal.display.enableAnimationsNote": "屬性、內容更新和擲骰的動畫效果",
"template.settingsModal.display.showPlotProgressionButtons": "顯示劇情推進按鈕(QR",
"template.settingsModal.display.showPlotProgressionButtonsNote": "在聊天輸入框上方顯示劇情推進提示按鈕(QR)",
"template.settingsModal.display.enableDebugMode": "Debug Mode",
"template.settingsModal.display.enableDebugModeNote": "UI 面板中顯示日誌,對於故障排除很有用。",
"template.settingsModal.advancedTitle": "進階",
"template.settingsModal.advanced.generationMode": "生成模式:",
"template.settingsModal.advanced.generationModeOptions.together": "同時生成",
"template.settingsModal.advanced.generationModeOptions.separate": "單獨生成",
{
"settings.language.label": "語言",
"settings.language.option.en": "English",
"settings.language.option.zh-tw": "繁體中文",
"settings.extensionEnabled": "啟用 RPG Companion",
"settings.note": "切換開關以啟用/停用 RPG Companion。其他設定可在面板內配置。",
"template.settingsTitle": "RPG Companion 設定",
"template.settingsModal.themeTitle": "主題",
"template.settingsModal.themeLabel": "可選主題:",
"template.settingsModal.themeOptions.default": "預設",
"template.settingsModal.themeOptions.sciFi": "科幻 (合成波)",
"template.settingsModal.themeOptions.fantasy": "奇幻 (古樸羊皮紙)",
"template.settingsModal.themeOptions.cyberpunk": "賽博朋克 (霓虹網格)",
"template.settingsModal.themeOptions.custom": "自訂",
"template.settingsModal.themeOptions.custom.background": "背景:",
"template.settingsModal.themeOptions.custom.accent": "強調色:",
"template.settingsModal.themeOptions.custom.text": "文字:",
"template.settingsModal.themeOptions.custom.highlight": "高亮:",
"template.settingsModal.theme.statBarLow": "屬性條顏色 (低)",
"template.settingsModal.theme.statBarLowNote": "屬性在 0% 時的顏色",
"template.settingsModal.theme.statBarHigh": "屬性條顏色 (高)",
"template.settingsModal.theme.statBarHighNote": "屬性在 100% 時的顏色",
"template.settingsModal.displayTitle": "顯示設定",
"template.settingsModal.displayNote": "使用擴充功能標籤來啟用/停用 RPG Companion 擴充功能。",
"template.settingsModal.display.panelPosition": "面板位置:",
"template.settingsModal.display.panelPositionOptions.right": "右側邊欄",
"template.settingsModal.display.panelPositionOptions.left": "左側邊欄",
"template.settingsModal.display.toggleAutoUpdate": "訊息後自動更新",
"template.settingsModal.display.showUserStats": "顯示 user 屬性",
"template.settingsModal.display.showInfoBox": "顯示資訊框",
"template.settingsModal.display.showPresentCharacters": "顯示在場角色",
"template.settingsModal.display.showInventory": "顯示物品欄",
"template.settingsModal.display.showThoughtsInChat": "在聊天中顯示想法",
"template.settingsModal.display.showThoughtsInChatNote": "將角色想法顯示為其訊息旁的泡泡",
"template.settingsModal.display.alwaysShowThoughtBubble": "始終顯示想法泡泡",
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自動展開想法泡泡",
"template.settingsModal.display.enableAnimations": "啟用動畫",
"template.settingsModal.display.enableAnimationsNote": "屬性、內容更新和擲骰的動畫效果",
"template.settingsModal.display.showImmersiveHtmlToggle": "顯示沉浸式 HTML",
"template.settingsModal.display.showSpotifyMusicToggle": "顯示 Spotify 音樂",
"template.settingsModal.display.showSnowflakesToggle": "顯示雪花效果",
"template.settingsModal.display.showPlotProgressionButtons": "顯示劇情推進按鈕(QR",
"template.settingsModal.display.showPlotProgressionButtonsNote": "在聊天輸入框上方顯示劇情推進提示按鈕(QR)",
"template.settingsModal.display.enableDebugMode": "Debug Mode",
"template.settingsModal.display.enableDebugModeNote": "UI 面板中顯示日誌,對於故障排除很有用。",
"template.settingsModal.advancedTitle": "進階",
"template.settingsModal.advanced.generationMode": "生成模式:",
"template.settingsModal.advanced.generationModeOptions.together": "同時生成",
"template.settingsModal.advanced.generationModeOptions.separate": "單獨生成",
"template.settingsModal.advanced.generationModeNote": "同時生成:將 RPG 追蹤添加到主要提示詞中一同生成。單獨生成:分開生成 RPG 數據。(就是手動或自動的差別)。外部 API:直接連接 OpenAI 兼容端點生成數據。",
"template.settingsModal.advanced.generationModeOptions.external": "外部 API",
"template.settingsModal.advanced.externalApi.title": "外部 API 設定",
@@ -57,120 +60,124 @@
"template.settingsModal.advanced.externalApi.temperature": "溫度 (Temperature)",
"template.settingsModal.advanced.externalApi.testConnection": "測試連接",
"template.settingsModal.advanced.contextMessages": "上下文訊息:",
"template.settingsModal.advanced.contextMessagesNote": "包含的最近訊息數量(僅限單獨生成模式)",
"template.settingsModal.advanced.memoryBatchSize": "記憶批次大小:",
"template.settingsModal.advanced.memoryBatchSizeNote": "在記憶回憶中每批處理的訊息數量",
"template.settingsModal.advanced.useSeparatePreset": "使用 RPG Companion 追蹤預設模型(設置次要模型)",
"template.settingsModal.advanced.useSeparatePresetNote": "僅限單獨生成模式。啟用後將使用“RPG Companion Trackers”預設中綁定的模型,而不是您的主要 API 模型。生成期間會自動切換預設,之後會恢復原使用預設。請在“RPG Companion Trackers”預設中選擇次要模型,並確保“將預設綁定到 API 連接”切換已開啟(在導入/導出預設按鈕旁邊)。",
"template.settingsModal.advanced.skipInjections": "在引導生成期間跳過注入:",
"template.settingsModal.advanced.skipInjectionsOptions.none": "從不跳過",
"template.settingsModal.advanced.skipInjectionsOptions.impersonation": "僅在模擬請求時跳過",
"template.settingsModal.advanced.skipInjectionsOptions.guided": "始終跳過引導",
"template.settingsModal.advanced.skipInjectionsNote": "當設置後,擴充功能在檢測到引導生成(通過 `instruct` 或 `quiet_prompt`)時,將根據所選模式不注入追蹤提示詞、範例或 HTML 指令。當與 GuidedGenerations 或類似擴充功能一起使用時非常有用。",
"template.settingsModal.advanced.customHtmlPromptTitle": "自訂 HTML 提示詞:",
"template.settingsModal.advanced.restoreDefaultHtmlPrompt": "恢復預設",
"template.settingsModal.advanced.customHtmlPromptNote": "自訂啟用“啟用沉浸式 HTML”時注入的 HTML 提示詞。上方顯示預設提示詞 - 您可以直接編輯或完全替換它。點擊“恢復預設”以重置。這會影響所有生成模式(同時、單獨和劇情推進)。",
"template.settingsModal.advanced.clearCache": "清除擴充功能快取",
"template.settingsModal.advanced.resetFabPositions": "重置按鈕位置",
"template.settingsModal.advanced.resetFabPositionsNote": "將所有浮動操作按鈕(切換、刷新、調試)重置為預設的左上位置。如果按鈕在螢幕外,這會很有用。",
"template.trackerEditorModal.title": "追蹤器編輯",
"template.trackerEditorModal.tabs.userStats": "User 屬性",
"template.trackerEditorModal.tabs.infoBox": "資訊框",
"template.trackerEditorModal.tabs.presentCharacters": "在場角色",
"template.trackerEditorModal.buttons.reset": "重置為預設值",
"template.trackerEditorModal.buttons.cancel": "取消",
"template.trackerEditorModal.buttons.save": "保存並應用",
"template.trackerEditorModal.userStatsTab.customStatsTitle": "自訂屬性",
"template.trackerEditorModal.userStatsTab.addCustomStatButton": "添加自訂屬性",
"template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG 屬性",
"template.trackerEditorModal.userStatsTab.enableRpgAttributes": "啟用 RPG 屬性",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "始終發送屬性(prompt",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "將 RPG 屬性始終包含在提示詞中,即使它們未顯示在面板上也一樣。",
"template.trackerEditorModal.userStatsTab.addAttributeButton": "添加屬性",
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "狀態欄",
"template.trackerEditorModal.userStatsTab.enableStatusSection": "啟用狀態欄",
"template.trackerEditorModal.userStatsTab.showMoodEmoji": "顯示心情emoji",
"template.trackerEditorModal.userStatsTab.statusFieldsLabel": "狀態欄欄位(以逗號分隔):",
"template.trackerEditorModal.userStatsTab.skillsSectionTitle": "技能欄",
"template.trackerEditorModal.userStatsTab.enableSkillsSection": "啟用技能欄",
"template.trackerEditorModal.userStatsTab.skillsLabelLabel": "技能欄標籤:",
"template.trackerEditorModal.userStatsTab.skillsListLabel": " 技能列表(以逗號分隔):",
"template.trackerEditorModal.infoBoxTab.widgetsTitle": "小工具",
"template.trackerEditorModal.infoBoxTab.dateWidget": "日期",
"template.trackerEditorModal.infoBoxTab.weatherWidget": "天氣",
"template.trackerEditorModal.infoBoxTab.temperatureWidget": "溫度",
"template.trackerEditorModal.infoBoxTab.timeWidget": "時間",
"template.trackerEditorModal.infoBoxTab.locationWidget": "位置",
"template.trackerEditorModal.infoBoxTab.recentEventsWidget": "近期事件",
"template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "關係狀態",
"template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "定義關係類型,並在角色頭像上顯示對應的表情符號",
"template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "新增關係類型",
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "外觀與當前行為舉止",
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "角色名稱下方顯示的字段,以 | 分隔。",
"template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "添加自訂字段",
"template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "內心話配置",
"template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "啟用角色內心話",
"template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "內心話標籤:",
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "內心話提示詞:",
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "角色屬性",
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "啟用角色屬性",
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "建立統計資料以追蹤每個角色(以彩色長條圖顯示)",
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "添加角色屬性",
"template.mainPanel.title": "RPG Companion",
"template.mainPanel.lastRoll": "上次擲骰:",
"template.mainPanel.clearLastRoll": "清除上次擲骰",
"template.mainPanel.enableImmersiveHtml": "啟用沉浸式 HTML",
"template.mainPanel.refreshRpgInfo": "刷新資訊",
"template.mainPanel.updating": "更新中...",
"template.mainPanel.editTrackersButton": "追蹤器編輯",
"template.mainPanel.settingsButton": "設定",
"global.none": "None",
"global.add": "添加",
"global.cancel": "取消",
"global.save": "保存",
"global.listView": "清單檢視",
"global.gridView": "格子檢視",
"global.status": "狀態欄",
"global.inventory": "物品欄",
"global.quests": "任務",
"global.info":"資訊",
"infobox.noData.title": "無資訊可顯示",
"infobox.noData.instruction": "在RP中產生新的回复,或在設定中切換到“單獨生成”,然後點擊“刷新資訊”按鈕。",
"infobox.recentEvents.title": "近期事件",
"infobox.recentEvents.addEventPlaceholder": "添加事件...",
"inventory.section.onPerson": "隨身物品",
"inventory.section.stored": "倉庫物品",
"inventory.section.assets": "資產",
"inventory.onPerson.empty": "這裡什麼都沒有 (⚲□⚲)",
"inventory.onPerson.title": "攜帶的物品",
"inventory.onPerson.addItemButton": "添加物品",
"inventory.onPerson.addItemPlaceholder": "輸入物品名稱...",
"inventory.stored.title": "倉庫位置",
"inventory.stored.addLocationButton": "添加倉庫",
"inventory.stored.addLocationPlaceholder": "輸入倉庫名稱...",
"inventory.stored.saveButton": "保存",
"inventory.stored.empty": "沒有倉庫 (⚲□⚲), 點擊\"添加倉庫\"來新增一個倉庫",
"inventory.stored.noItems": "這個倉庫是空的 (⚲□⚲)",
"inventory.stored.addItemToLocationPlaceholder": "輸入物品名稱...",
"inventory.stored.addItemButton": "添加物品",
"inventory.stored.confirmRemoveLocationMessage": "確定要刪除這個倉庫嗎?這將移除所有其中的物品。",
"inventory.stored.confirmRemoveLocationConfirmButton": "刪除",
"inventory.assets.empty": "沒有資產 (⚲□⚲) 好窮",
"inventory.assets.title": "車輛、房產及主要財產",
"inventory.assets.addAssetModalTitle": "添加資產",
"inventory.assets.addAssetButton": "添加資產",
"inventory.assets.addAssetPlaceholder": "輸入資產名稱...",
"inventory.assets.description": "資產包括車輛(汽車、摩托車)、房產(房屋、公寓)和主要設備(車間工具、特殊物品)。",
"quests.section.main": "主線任務",
"quests.section.optional": "支線任務",
"quests.main.title": "主線任務",
"quests.main.addQuestButton": "添加主要任務",
"quests.main.addQuestPlaceholder": "輸入主線任務名稱...",
"quests.main.empty": "當前無主要任務 (ฅ˙Ⱉ˙ฅ)",
"quests.main.hint": "主線任務代表你在故事中的主要目標。",
"quests.optional.title": "支線任務",
"quests.optional.addQuestButton": "添加支線任務",
"quests.optional.addQuestPlaceholder": "輸入支線任務名稱...",
"quests.optional.empty": "當前無支線任務 (ʘ̆ʚʘ̆)",
"quests.optional.hint": "支線任務是補充主線劇情的支線目標。"
}
"template.settingsModal.advanced.contextMessagesNote": "包含的最近訊息數量(僅限單獨生成模式)",
"template.settingsModal.advanced.memoryBatchSize": "記憶批次大小:",
"template.settingsModal.advanced.memoryBatchSizeNote": "在記憶回憶中每批處理的訊息數量",
"template.settingsModal.advanced.useSeparatePreset": "使用 RPG Companion 追蹤預設模型(設置次要模型)",
"template.settingsModal.advanced.useSeparatePresetNote": "僅限單獨生成模式。啟用後將使用“RPG Companion Trackers”預設中綁定的模型,而不是您的主要 API 模型。生成期間會自動切換預設,之後會恢復原使用預設。請在“RPG Companion Trackers”預設中選擇次要模型,並確保“將預設綁定到 API 連接”切換已開啟(在導入/導出預設按鈕旁邊)。",
"template.settingsModal.advanced.skipInjections": "在引導生成期間跳過注入:",
"template.settingsModal.advanced.skipInjectionsOptions.none": "從不跳過",
"template.settingsModal.advanced.skipInjectionsOptions.impersonation": "僅在模擬請求時跳過",
"template.settingsModal.advanced.skipInjectionsOptions.guided": "始終跳過引導",
"template.settingsModal.advanced.skipInjectionsNote": "當設置後,擴充功能在檢測到引導生成(通過 `instruct` 或 `quiet_prompt`)時,將根據所選模式不注入追蹤提示詞、範例或 HTML 指令。當與 GuidedGenerations 或類似擴充功能一起使用時非常有用。",
"template.settingsModal.advanced.customHtmlPromptTitle": "自訂 HTML 提示詞:",
"template.settingsModal.advanced.restoreDefaultHtmlPrompt": "恢復預設",
"template.settingsModal.advanced.customHtmlPromptNote": "自訂啟用“啟用沉浸式 HTML”時注入的 HTML 提示詞。上方顯示預設提示詞 - 您可以直接編輯或完全替換它。點擊“恢復預設”以重置。這會影響所有生成模式(同時、單獨和劇情推進)。",
"template.settingsModal.advanced.clearCache": "清除擴充功能快取",
"template.settingsModal.advanced.resetFabPositions": "重置按鈕位置",
"template.settingsModal.advanced.resetFabPositionsNote": "將所有浮動操作按鈕(切換、刷新、調試)重置為預設的左上位置。如果按鈕在螢幕外,這會很有用。",
"template.trackerEditorModal.title": "追蹤器編輯",
"template.trackerEditorModal.tabs.userStats": "User 屬性",
"template.trackerEditorModal.tabs.infoBox": "資訊框",
"template.trackerEditorModal.tabs.presentCharacters": "在場角色",
"template.trackerEditorModal.buttons.reset": "重置為預設值",
"template.trackerEditorModal.buttons.cancel": "取消",
"template.trackerEditorModal.buttons.save": "保存並應用",
"template.trackerEditorModal.userStatsTab.customStatsTitle": "自訂屬性",
"template.trackerEditorModal.userStatsTab.addCustomStatButton": "添加自訂屬性",
"template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG 屬性",
"template.trackerEditorModal.userStatsTab.enableRpgAttributes": "啟用 RPG 屬性",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "始終發送屬性(prompt",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "將 RPG 屬性始終包含在提示詞中,即使它們未顯示在面板上也一樣。",
"template.trackerEditorModal.userStatsTab.addAttributeButton": "添加屬性",
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "狀態欄",
"template.trackerEditorModal.userStatsTab.enableStatusSection": "啟用狀態欄",
"template.trackerEditorModal.userStatsTab.showMoodEmoji": "顯示心情emoji",
"template.trackerEditorModal.userStatsTab.statusFieldsLabel": "狀態欄欄位(以逗號分隔):",
"template.trackerEditorModal.userStatsTab.skillsSectionTitle": "技能欄",
"template.trackerEditorModal.userStatsTab.enableSkillsSection": "啟用技能欄",
"template.trackerEditorModal.userStatsTab.skillsLabelLabel": "技能欄標籤:",
"template.trackerEditorModal.userStatsTab.skillsListLabel": " 技能列表(以逗號分隔):",
"template.trackerEditorModal.infoBoxTab.widgetsTitle": "小工具",
"template.trackerEditorModal.infoBoxTab.dateWidget": "日期",
"template.trackerEditorModal.infoBoxTab.weatherWidget": "天氣",
"template.trackerEditorModal.infoBoxTab.temperatureWidget": "溫度",
"template.trackerEditorModal.infoBoxTab.timeWidget": "時間",
"template.trackerEditorModal.infoBoxTab.locationWidget": "位置",
"template.trackerEditorModal.infoBoxTab.recentEventsWidget": "近期事件",
"template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "關係狀態",
"template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "定義關係類型,並在角色頭像上顯示對應的表情符號",
"template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "新增關係類型",
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "外觀與當前行為舉止",
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "角色名稱下方顯示的字段,以 | 分隔。",
"template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "添加自訂字段",
"template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "內心話配置",
"template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "啟用角色內心話",
"template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "內心話標籤:",
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "內心話提示詞:",
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "角色屬性",
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "啟用角色屬性",
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "建立統計資料以追蹤每個角色(以彩色長條圖顯示)",
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "添加角色屬性",
"template.mainPanel.title": "RPG Companion",
"template.mainPanel.lastRoll": "上次擲骰:",
"template.mainPanel.clearLastRoll": "清除上次擲骰",
"template.mainPanel.immersiveHtml": "沉浸式 HTML",
"template.mainPanel.spotifyMusic": "Spotify 音樂",
"template.mainPanel.snowflakesEffect": "雪花效果",
"template.mainPanel.refreshRpgInfo": "刷新資訊",
"template.mainPanel.updating": "更新中...",
"template.mainPanel.editTrackersButton": "追蹤器編輯",
"template.mainPanel.settingsButton": "設定",
"global.none": "None",
"global.add": "添加",
"global.cancel": "取消",
"global.save": "保存",
"global.listView": "清單檢視",
"global.gridView": "格子檢視",
"global.status": "狀態欄",
"global.inventory": "物品欄",
"global.quests": "任務",
"global.info":"資訊",
"infobox.noData.title": "無資訊可顯示",
"infobox.noData.instruction": "在RP中產生新的回复,或在設定中切換到“單獨生成”,然後點擊“刷新資訊”按鈕。",
"infobox.recentEvents.title": "近期事件",
"infobox.recentEvents.addEventPlaceholder": "添加事件...",
"inventory.section.onPerson": "隨身物品",
"inventory.section.stored": "倉庫物品",
"inventory.section.assets": "資產",
"inventory.onPerson.empty": "這裡什麼都沒有 (⚲□⚲)",
"inventory.onPerson.title": "攜帶的物品",
"inventory.onPerson.addItemButton": "添加物品",
"inventory.onPerson.addItemPlaceholder": "輸入物品名稱...",
"inventory.stored.title": "倉庫位置",
"inventory.stored.addLocationButton": "添加倉庫",
"inventory.stored.addLocationPlaceholder": "輸入倉庫名稱...",
"inventory.stored.saveButton": "保存",
"inventory.stored.empty": "沒有倉庫 (⚲□⚲), 點擊\"添加倉庫\"來新增一個倉庫",
"inventory.stored.noItems": "這個倉庫是空的 (⚲□⚲)",
"inventory.stored.addItemToLocationPlaceholder": "輸入物品名稱...",
"inventory.stored.addItemButton": "添加物品",
"inventory.stored.confirmRemoveLocationMessage": "確定要刪除這個倉庫嗎?這將移除所有其中的物品。",
"inventory.stored.confirmRemoveLocationConfirmButton": "刪除",
"inventory.assets.empty": "沒有資產 (⚲□⚲) 好窮",
"inventory.assets.title": "車輛、房產及主要財產",
"inventory.assets.addAssetModalTitle": "添加資產",
"inventory.assets.addAssetButton": "添加資產",
"inventory.assets.addAssetPlaceholder": "輸入資產名稱...",
"inventory.assets.description": "資產包括車輛(汽車、摩托車)、房產(房屋、公寓)和主要設備(車間工具、特殊物品)。",
"quests.section.main": "主線任務",
"quests.section.optional": "支線任務",
"quests.main.title": "主線任務",
"quests.main.addQuestButton": "添加主要任務",
"quests.main.addQuestPlaceholder": "輸入主線任務名稱...",
"quests.main.empty": "當前無主要任務 (ฅ˙Ⱉ˙ฅ)",
"quests.main.hint": "主線任務代表你在故事中的主要目標。",
"quests.optional.title": "支線任務",
"quests.optional.addQuestButton": "添加支線任務",
"quests.optional.addQuestPlaceholder": "輸入支線任務名稱...",
"quests.optional.empty": "當前無支線任務 (ʘ̆ʚʘ̆)",
"quests.optional.hint": "支線任務是補充主線劇情的支線目標。",
"musicPlayer.title": "場景音樂",
"musicPlayer.noMusic": "AI 會在適當時為場景建議音樂"
}
+79
View File
@@ -0,0 +1,79 @@
/**
* Music Player Module
* Handles parsing and storing Spotify URLs from AI responses
*/
import { extensionSettings, committedTrackerData } from '../../core/state.js';
/**
* Extracts song suggestion from AI response in <spotify:Song - Artist/> format
* @param {string} responseText - The raw AI response text
* @returns {Object|null} Object with {song, artist, searchQuery} or null if not found
*/
export function extractSpotifyUrl(responseText) {
if (!responseText || !extensionSettings.enableSpotifyMusic) return null;
// Match <spotify:Song Title - Artist Name/> format
const songMatch = responseText.match(/<spotify:([^<>-]+)\s*-\s*([^<>\/]+)\/>/i);
if (songMatch) {
const song = songMatch[1].trim();
const artist = songMatch[2].trim();
const searchQuery = `${song} ${artist}`;
return {
song,
artist,
searchQuery,
displayText: `${song} - ${artist}`
};
}
return null;
}
/**
* Converts song data to Spotify app protocol URL
* @param {Object} songData - Object with {song, artist, searchQuery}
* @returns {string} Spotify app protocol URL
*/
export function convertToEmbedUrl(songData) {
if (!songData || !songData.searchQuery) return '';
// Use Spotify app protocol for direct app opening
const encodedQuery = encodeURIComponent(songData.searchQuery);
return `spotify:search:${encodedQuery}`;
}
/**
* Parses AI response for song suggestion and stores it
* @param {string} responseText - The raw AI response text
* @returns {boolean} True if song was found and stored
*/
export function parseAndStoreSpotifyUrl(responseText) {
if (!extensionSettings.enableSpotifyMusic) return false;
const songData = extractSpotifyUrl(responseText);
console.log('[RPG Companion] Spotify Parser: Found song:', songData);
if (songData) {
// Store in committed tracker data
committedTrackerData.spotifyUrl = songData;
console.log('[RPG Companion] Spotify Parser: Stored song in committedTrackerData:', committedTrackerData.spotifyUrl);
return true;
}
return false;
}
/**
* Gets the current song data from committed tracker data
* @returns {Object|null} Current song data or null
*/
export function getCurrentSpotifyUrl() {
return committedTrackerData.spotifyUrl || null;
}
/**
* Clears the current song data
*/
export function clearSpotifyUrl() {
committedTrackerData.spotifyUrl = null;
}
+9 -3
View File
@@ -12,18 +12,21 @@ import {
isGenerating,
lastActionWasSwipe,
setIsGenerating,
setLastActionWasSwipe
setLastActionWasSwipe,
$musicPlayerContainer
} from '../../core/state.js';
import { saveChatData } from '../../core/persistence.js';
import {
generateSeparateUpdatePrompt
} from './promptBuilder.js';
import { parseResponse, parseUserStats } from './parser.js';
import { parseAndStoreSpotifyUrl } from '../features/musicPlayer.js';
import { renderUserStats } from '../rendering/userStats.js';
import { renderInfoBox } from '../rendering/infoBox.js';
import { renderThoughts } from '../rendering/thoughts.js';
import { renderInventory } from '../rendering/inventory.js';
import { renderQuests } from '../rendering/quests.js';
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
import { i18n } from '../../core/i18n.js';
import { generateAvatarsForCharacters } from '../features/avatarGenerator.js';
@@ -121,7 +124,7 @@ export async function testExternalAPIConnection() {
if (!baseUrl || !apiKey || !model) {
return {
success: false,
message: !apiKey
message: !apiKey
? 'API Key not found. Please re-enter it in settings (keys are stored locally per-browser).'
: 'Please fill in all required fields (Base URL, API Key, and Model)'
};
@@ -267,6 +270,8 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
if (response) {
// console.log('[RPG Companion] Raw AI response:', response);
const parsedData = parseResponse(response);
// Parse and store Spotify URL if feature is enabled
parseAndStoreSpotifyUrl(response);
// console.log('[RPG Companion] Parsed data:', parsedData);
// console.log('[RPG Companion] parsedData.userStats:', parsedData.userStats ? parsedData.userStats.substring(0, 100) + '...' : 'null');
@@ -346,6 +351,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Save to chat metadata
saveChatData();
@@ -356,7 +362,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
const charactersNeedingAvatars = parseCharactersFromThoughts(parsedData.characterThoughts);
if (charactersNeedingAvatars.length > 0) {
console.log('[RPG Companion] Generating avatars for:', charactersNeedingAvatars);
// Generate avatars - this awaits completion
await generateAvatarsForCharacters(charactersNeedingAvatars, (names) => {
// Callback when generation starts - re-render to show loading spinners
+32 -1
View File
@@ -19,7 +19,9 @@ import {
generateTrackerExample,
generateTrackerInstructions,
generateContextualSummary,
DEFAULT_HTML_PROMPT
DEFAULT_HTML_PROMPT,
DEFAULT_SPOTIFY_PROMPT,
SPOTIFY_FORMAT_INSTRUCTION
} from './promptBuilder.js';
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
@@ -67,6 +69,7 @@ export async function onGenerationStarted(type, data) {
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
}
@@ -230,6 +233,19 @@ export async function onGenerationStarted(type, data) {
// Clear HTML prompt if disabled
setExtensionPrompt('rpg-companion-html', '', 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
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for together mode');
} else {
// Clear Spotify prompt if disabled
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
}
} else if (extensionSettings.generationMode === 'separate') {
// In SEPARATE mode, inject the contextual summary for main roleplay generation
const contextSummary = generateContextualSummary();
@@ -266,6 +282,19 @@ Ensure these details naturally reflect and influence the narrative. Character be
setExtensionPrompt('rpg-companion-html', '', 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
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for separate mode');
} else {
// Clear Spotify prompt if disabled
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
}
// Clear together mode injections
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
@@ -274,5 +303,7 @@ Ensure these details naturally reflect and influence the narrative. Character be
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
}
}
+24
View File
@@ -16,6 +16,16 @@ import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../co
*/
export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, and JS segments whenever they enhance visual storytelling (e.g., for in-world screens, posters, books, letters, signs, crests, labels, etc.). Style them to match the setting's theme (e.g., fantasy, sci-fi), keep the text readable, and embed all assets directly (using inline SVGs only with no external scripts, libraries, or fonts). Use these elements freely and naturally within the narrative as characters would encounter them, including animations, 3D effects, pop-ups, dropdowns, websites, and so on. Do not wrap the HTML/CSS/JS in code fences!`;
/**
* Default Spotify music prompt text (customizable by users)
*/
export const DEFAULT_SPOTIFY_PROMPT = `If appropriate for the current scene's mood and atmosphere, suggest a song that fits the ambiance. Choose music that enhances the emotional tone, setting, or action of the scene.`;
/**
* Spotify format instruction (constant, not editable by users)
*/
export const SPOTIFY_FORMAT_INSTRUCTION = `Include it in this exact format: <spotify:Song Title - Artist Name/>.`;
/**
* Gets character card information for current chat (handles both single and group chats)
* @returns {string} Formatted character information
@@ -445,6 +455,20 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
instructions += htmlPrompt;
}
// Append Spotify music prompt if enabled AND includeHtmlPrompt is true
if (extensionSettings.enableSpotifyMusic && includeHtmlPrompt) {
// Add separator
if (hasAnyTrackers || extensionSettings.enableHtmlPrompt) {
instructions += `\n\n`;
} else {
instructions += `\n`;
}
// Use custom Spotify prompt if set, otherwise use default
const spotifyPrompt = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
instructions += spotifyPrompt + ' ' + SPOTIFY_FORMAT_INSTRUCTION;
}
return instructions;
}
+32 -6
View File
@@ -16,12 +16,14 @@ import {
setLastActionWasSwipe,
setIsPlotProgression,
updateLastGeneratedData,
updateCommittedTrackerData
updateCommittedTrackerData,
$musicPlayerContainer
} from '../../core/state.js';
import { saveChatData, loadChatData } from '../../core/persistence.js';
// Generation & Parsing
import { parseResponse, parseUserStats } from '../generation/parser.js';
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
import { updateRPGData } from '../generation/apiClient.js';
// Rendering
@@ -30,6 +32,7 @@ import { renderInfoBox } from '../rendering/infoBox.js';
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
import { renderInventory } from '../rendering/inventory.js';
import { renderQuests } from '../rendering/quests.js';
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
// Utils
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
@@ -119,6 +122,8 @@ export async function onMessageReceived(data) {
// console.log('[RPG Companion] Parsing together mode response:', responseText);
const parsedData = parseResponse(responseText);
// Parse and store Spotify URL if feature is enabled
parseAndStoreSpotifyUrl(responseText);
// console.log('[RPG Companion] Parsed data:', parsedData);
// Update stored data
@@ -176,6 +181,7 @@ export async function onMessageReceived(data) {
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
}
// Note: <trackers> XML tags are automatically hidden by SillyTavern
// Note: <Song - Artist/> tags are also automatically hidden by SillyTavern
// Update the message in chat history
lastMessage.mes = cleanedMessage.trim();
@@ -191,6 +197,7 @@ export async function onMessageReceived(data) {
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Then update the DOM to reflect the cleaned message
// Using updateMessageBlock to perform macro substitutions + regex formatting
@@ -202,11 +209,28 @@ export async function onMessageReceived(data) {
// Save to chat metadata
saveChatData();
}
} else if (extensionSettings.generationMode === 'separate' && extensionSettings.autoUpdate) {
// In separate mode with auto-update, trigger update after message
setTimeout(async () => {
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
}, 500);
} else if (extensionSettings.generationMode === 'separate') {
// In separate mode, also parse Spotify URLs from the main roleplay response
const lastMessage = chat[chat.length - 1];
if (lastMessage && !lastMessage.is_user) {
const responseText = lastMessage.mes;
// Parse and store Spotify URL
const foundSpotifyUrl = parseAndStoreSpotifyUrl(responseText);
// No need to clean message - SillyTavern auto-hides <Song - Artist/> tags
if (foundSpotifyUrl && extensionSettings.enableSpotifyMusic) {
// Just render the music player
renderMusicPlayer($musicPlayerContainer[0]);
}
}
// Trigger auto-update if enabled
if (extensionSettings.autoUpdate) {
setTimeout(async () => {
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
}, 500);
}
}
// Reset the swipe flag after generation completes
@@ -253,6 +277,7 @@ export function onCharacterChanged() {
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update chat thought overlays
updateChatThoughts();
@@ -328,6 +353,7 @@ export function onMessageSwiped(messageIndex) {
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update chat thought overlays
updateChatThoughts();
+150
View File
@@ -0,0 +1,150 @@
/**
* Music Player Rendering Module
* Handles UI rendering for Spotify music player widget
*/
import { extensionSettings, committedTrackerData } from '../../core/state.js';
import { i18n } from '../../core/i18n.js';
/**
* Creates a Spotify deep link URL that opens the Spotify app
* Uses spotify:search: protocol for app, falls back to web URL
* @param {Object} songData - Object with {song, artist, searchQuery}
* @returns {Object} Object with appUrl and webUrl
*/
function createSpotifyUrls(songData) {
if (!songData || !songData.searchQuery) {
return { appUrl: '', webUrl: '' };
}
const encodedQuery = encodeURIComponent(songData.searchQuery);
return {
// Spotify app protocol - opens directly in Spotify app on desktop/mobile
appUrl: `spotify:search:${encodedQuery}`,
// Web fallback - opens Spotify web player search
webUrl: `https://open.spotify.com/search/${encodedQuery}`
};
}
/**
* Opens Spotify with the given song
* Tries app protocol first, falls back to web
* @param {Object} songData - Song data object
*/
function openInSpotify(songData) {
const urls = createSpotifyUrls(songData);
// Try to open in Spotify app first
// On mobile, this will open the Spotify app if installed
// On desktop, this will open Spotify desktop app if installed
window.location.href = urls.appUrl;
// Fallback: If app doesn't open within 2 seconds, open web version
// This handles cases where Spotify app isn't installed
setTimeout(() => {
// Check if we're still on the same page (app didn't open)
// Note: This is a best-effort fallback
if (document.hasFocus()) {
window.open(urls.webUrl, '_blank');
}
}, 1500);
}
/**
* Renders the Spotify music player as a mini player widget above chat input
* @param {HTMLElement} container - Container element to render into
*/
export function renderMusicPlayer(container) {
console.log('[RPG Companion] Music Player: renderMusicPlayer called');
// Remove old chat-attached player if it exists
$('#rpg-chat-music-player').remove();
console.log('[RPG Companion] Music Player: enableSpotifyMusic =', extensionSettings.enableSpotifyMusic);
if (!extensionSettings.enableSpotifyMusic) {
console.warn('[RPG Companion] Music Player: Spotify music is disabled');
return;
}
const songData = committedTrackerData.spotifyUrl;
console.log('[RPG Companion] Music Player: Rendering with song:', songData);
if (!songData || !songData.displayText) {
// No song - don't show anything
return;
}
// Create the mini music player widget
const musicPlayerHtml = `
<div id="rpg-chat-music-player" class="rpg-music-widget">
<div class="rpg-music-widget-content">
<div class="rpg-music-widget-icon">
<i class="fa-brands fa-spotify"></i>
</div>
<div class="rpg-music-widget-info">
<div class="rpg-music-widget-title" title="${songData.song}">${songData.song}</div>
<div class="rpg-music-widget-artist" title="${songData.artist}">${songData.artist}</div>
</div>
<button class="rpg-music-widget-play" title="Play in Spotify">
<i class="fa-solid fa-play"></i>
</button>
<button class="rpg-music-widget-close" title="Dismiss">
<i class="fa-solid fa-times"></i>
</button>
</div>
</div>
`;
// Find the chat form container and insert widget before (above) it
const $chatForm = $('#send_form');
console.log('[RPG Companion] Music Player: Found #send_form:', $chatForm.length > 0);
if ($chatForm.length === 0) {
console.error('[RPG Companion] Music Player: Could not find #send_form - cannot render widget!');
return;
}
// Insert widget inside (at top of) the chat form
console.log('[RPG Companion] Music Player: Prepending widget to #send_form');
$chatForm.prepend(musicPlayerHtml);
console.log('[RPG Companion] Music Player: Widget inserted, checking if visible...');
const $widget = $('#rpg-chat-music-player');
console.log('[RPG Companion] Music Player: Widget exists:', $widget.length > 0);
if ($widget.length > 0) {
console.log('[RPG Companion] Music Player: Widget position:', $widget.offset());
console.log('[RPG Companion] Music Player: Widget dimensions:', { width: $widget.width(), height: $widget.height() });
console.log('[RPG Companion] Music Player: Widget CSS display:', $widget.css('display'));
console.log('[RPG Companion] Music Player: Widget CSS visibility:', $widget.css('visibility'));
}
// Bind play button click
$('#rpg-chat-music-player .rpg-music-widget-play').on('click', function(e) {
e.stopPropagation();
openInSpotify(songData);
});
// Bind close button click
$('#rpg-chat-music-player .rpg-music-widget-close').on('click', function(e) {
e.stopPropagation();
$('#rpg-chat-music-player').fadeOut(200, function() {
$(this).remove();
});
});
// Clicking anywhere else on the widget also opens Spotify
$('#rpg-chat-music-player .rpg-music-widget-content').on('click', function() {
openInSpotify(songData);
});
}
/**
* Updates the music player display
* @param {HTMLElement} container - Container element
*/
export function updateMusicPlayer(container) {
renderMusicPlayer(container);
}
+22 -5
View File
@@ -10,7 +10,8 @@ import {
$infoBoxContainer,
$thoughtsContainer,
$inventoryContainer,
$questsContainer
$questsContainer,
$musicPlayerContainer
} from '../../core/state.js';
import { i18n } from '../../core/i18n.js';
@@ -283,10 +284,18 @@ export function updateSectionVisibility() {
}
}
if ($musicPlayerContainer) {
if (extensionSettings.enableSpotifyMusic) {
$musicPlayerContainer.show();
} else {
$musicPlayerContainer.hide();
}
}
// Show/hide dividers intelligently
// Divider after User Stats: shown if User Stats is visible AND at least one section after it is visible
const showDividerAfterStats = extensionSettings.showUserStats &&
(extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests);
(extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
if (showDividerAfterStats) {
$('#rpg-divider-stats').show();
} else {
@@ -304,20 +313,28 @@ export function updateSectionVisibility() {
// Divider after Thoughts: shown if Thoughts is visible AND at least one section after it is visible
const showDividerAfterThoughts = extensionSettings.showCharacterThoughts &&
(extensionSettings.showInventory || extensionSettings.showQuests);
(extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
if (showDividerAfterThoughts) {
$('#rpg-divider-thoughts').show();
} else {
$('#rpg-divider-thoughts').hide();
}
// Divider after Inventory: shown if Inventory is visible AND Quests is visible
const showDividerAfterInventory = extensionSettings.showInventory && extensionSettings.showQuests;
// Divider after Inventory: shown if Inventory is visible AND (Quests or Music) is visible
const showDividerAfterInventory = extensionSettings.showInventory && (extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
if (showDividerAfterInventory) {
$('#rpg-divider-inventory').show();
} else {
$('#rpg-divider-inventory').hide();
}
// Divider after Quests: shown if Quests is visible AND Music is visible
const showDividerAfterQuests = extensionSettings.showQuests && extensionSettings.enableSpotifyMusic;
if (showDividerAfterQuests) {
$('#rpg-divider-quests').show();
} else {
$('#rpg-divider-quests').hide();
}
}
/**
+10 -1
View File
@@ -4,7 +4,7 @@
*/
import { extensionSettings } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js';
import { DEFAULT_HTML_PROMPT } from '../generation/promptBuilder.js';
import { DEFAULT_HTML_PROMPT, DEFAULT_SPOTIFY_PROMPT } from '../generation/promptBuilder.js';
let $editorModal = null;
let tempPrompts = null; // Temporary prompts for cancel functionality
@@ -12,6 +12,7 @@ let tempPrompts = null; // Temporary prompts for cancel functionality
// Default prompts
const DEFAULT_PROMPTS = {
html: DEFAULT_HTML_PROMPT,
spotify: DEFAULT_SPOTIFY_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, but 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.
@@ -97,6 +98,7 @@ function openPromptsEditor() {
// Create temporary copy for cancel functionality
tempPrompts = {
html: extensionSettings.customHtmlPrompt || '',
spotify: extensionSettings.customSpotifyPrompt || '',
plotRandom: extensionSettings.customPlotRandomPrompt || '',
plotNatural: extensionSettings.customPlotNaturalPrompt || '',
avatar: extensionSettings.avatarLLMCustomInstruction || '',
@@ -107,6 +109,7 @@ function openPromptsEditor() {
// Load current values or defaults
$('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html);
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
$('#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);
@@ -141,6 +144,7 @@ function closePromptsEditor() {
*/
function savePrompts() {
extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim();
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').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();
@@ -164,6 +168,9 @@ function restorePromptToDefault(promptType) {
case 'html':
extensionSettings.customHtmlPrompt = '';
break;
case 'spotify':
extensionSettings.customSpotifyPrompt = '';
break;
case 'plotRandom':
extensionSettings.customPlotRandomPrompt = '';
break;
@@ -192,6 +199,7 @@ function restorePromptToDefault(promptType) {
*/
function restoreAllToDefaults() {
$('#rpg-prompt-html').val(DEFAULT_PROMPTS.html);
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
$('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural);
$('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar);
@@ -201,6 +209,7 @@ function restoreAllToDefaults() {
// Clear all custom prompts
extensionSettings.customHtmlPrompt = '';
extensionSettings.customSpotifyPrompt = '';
extensionSettings.customPlotRandomPrompt = '';
extensionSettings.customPlotNaturalPrompt = '';
extensionSettings.avatarLLMCustomInstruction = '';
+78
View File
@@ -0,0 +1,78 @@
/**
* Snowflakes Effect Module
* Creates and manages animated snowflakes overlay
*/
import { extensionSettings } from '../../core/state.js';
let snowflakesContainer = null;
/**
* Create snowflakes container and snowflakes
*/
function createSnowflakes() {
if (snowflakesContainer) return; // Already created
// Create container
snowflakesContainer = document.createElement('div');
snowflakesContainer.className = 'rpg-snowflakes-container';
// Create 50 snowflakes with random positions
for (let i = 0; i < 50; i++) {
const snowflake = document.createElement('div');
snowflake.className = 'rpg-snowflake';
snowflake.textContent = '❄';
// Random horizontal position
snowflake.style.left = `${Math.random() * 100}%`;
// Random animation delay for staggered effect
snowflake.style.animationDelay = `${Math.random() * 10}s`;
// Random animation duration (between 10-20s)
snowflake.style.animationDuration = `${10 + Math.random() * 10}s`;
snowflakesContainer.appendChild(snowflake);
}
document.body.appendChild(snowflakesContainer);
}
/**
* Remove snowflakes container
*/
function removeSnowflakes() {
if (snowflakesContainer) {
snowflakesContainer.remove();
snowflakesContainer = null;
}
}
/**
* Toggle snowflakes effect
*/
export function toggleSnowflakes(enabled) {
if (enabled) {
createSnowflakes();
} else {
removeSnowflakes();
}
}
/**
* Initialize snowflakes based on saved state
*/
export function initSnowflakes() {
const enabled = extensionSettings.enableSnowflakes || false;
if (enabled) {
createSnowflakes();
}
}
/**
* Clean up snowflakes
*/
export function cleanupSnowflakes() {
removeSnowflakes();
}
+21
View File
@@ -76,6 +76,27 @@ export function toggleAnimations() {
}
}
/**
* Updates visibility of feature toggles in main panel based on settings
*/
export function updateFeatureTogglesVisibility() {
const $featuresRow = $('#rpg-features-row');
const $htmlToggle = $('#rpg-html-toggle-wrapper');
const $spotifyToggle = $('#rpg-spotify-toggle-wrapper');
const $snowflakesToggle = $('#rpg-snowflakes-toggle-wrapper');
// Show/hide individual toggles
$htmlToggle.toggle(extensionSettings.showHtmlToggle);
$spotifyToggle.toggle(extensionSettings.showSpotifyToggle);
$snowflakesToggle.toggle(extensionSettings.showSnowflakesToggle);
// Hide entire row if all toggles are hidden
const anyVisible = extensionSettings.showHtmlToggle ||
extensionSettings.showSpotifyToggle ||
extensionSettings.showSnowflakesToggle;
$featuresRow.toggle(anyVisible);
}
/**
* Updates the settings popup theme in real-time.
* Backwards compatible wrapper for SettingsModal class.
+378 -10
View File
@@ -1251,7 +1251,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
/* Weather Widget Icon */
.rpg-weather-icon {
font-size: clamp(14px, 2.5vw, 20px);
font-size: clamp(0.875rem, 2.5vw, 1.25rem);
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
flex-shrink: 0;
line-height: 1;
@@ -1267,12 +1267,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
opacity: 0.85;
line-height: 1.2;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: normal;
flex-shrink: 1;
}
@@ -5120,19 +5117,19 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
}
.rpg-weather-forecast {
font-size: clamp(9px, 2.2vw, 11px) !important;
font-size: clamp(0.5625rem, 2.2vw, 0.6875rem) !important;
}
.rpg-temp-value {
font-size: clamp(10px, 2.5vw, 13px) !important;
font-size: clamp(0.625rem, 2.5vw, 0.8125rem) !important;
}
.rpg-time-value {
font-size: clamp(10px, 2.5vw, 13px) !important;
font-size: clamp(0.625rem, 2.5vw, 0.8125rem) !important;
}
.rpg-location-text {
font-size: clamp(11px, 2.8vw, 14px) !important;
font-size: clamp(0.6875rem, 2.8vw, 0.875rem) !important;
}
.rpg-map-marker {
@@ -8303,3 +8300,374 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
font-size: 14px;
}
}
/* ============================================
SPOTIFY MUSIC WIDGET STYLES
============================================ */
/* Music Widget Container - Positioned above chat input */
.rpg-music-widget {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, calc(-100% - 8px));
width: calc(100% - 40px);
max-width: 650px;
z-index: 1000;
animation: rpg-music-slide-in 0.3s ease-out;
}
@keyframes rpg-music-slide-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Widget Content - The actual player UI */
.rpg-music-widget-content {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: var(--rpg-accent);
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 2px solid var(--rpg-border);
}
.rpg-music-widget-content:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3), 0 4px 8px var(--rpg-highlight);
}
/* Spotify Icon */
.rpg-music-widget-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
border-radius: 50%;
color: #1DB954;
font-size: 20px;
}
/* Song Info Container */
.rpg-music-widget-info {
flex: 1;
min-width: 0; /* Allows text truncation */
display: flex;
flex-direction: column;
gap: 2px;
}
/* Song Title */
.rpg-music-widget-title {
color: var(--rpg-text);
font-size: 14px;
font-weight: 700;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
/* Artist Name */
.rpg-music-widget-artist {
color: var(--rpg-text);
opacity: 0.7;
font-size: 12px;
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Play Button */
.rpg-music-widget-play {
flex-shrink: 0;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: var(--rpg-highlight);
border: none;
border-radius: 50%;
color: var(--rpg-text);
font-size: 14px;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.rpg-music-widget-play:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.rpg-music-widget-play:active {
transform: scale(0.95);
}
.rpg-music-widget-play i {
margin-left: 2px; /* Visual centering for play icon */
}
/* Close/Dismiss Button */
.rpg-music-widget-close {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.3);
border: none;
border-radius: 50%;
color: var(--rpg-text);
opacity: 0.6;
font-size: 12px;
cursor: pointer;
transition: background 0.15s ease, opacity 0.15s ease;
}
.rpg-music-widget-close:hover {
background: rgba(0, 0, 0, 0.5);
opacity: 1;
}
/* Mobile Responsive Styles */
@media (max-width: 600px) {
.rpg-music-widget-content {
padding: 8px 12px;
gap: 10px;
}
.rpg-music-widget-icon {
width: 36px;
height: 36px;
font-size: 18px;
}
.rpg-music-widget-title {
font-size: 13px;
}
.rpg-music-widget-artist {
font-size: 11px;
}
.rpg-music-widget-play {
width: 32px;
height: 32px;
font-size: 12px;
}
.rpg-music-widget-close {
width: 22px;
height: 22px;
font-size: 11px;
}
}
/* Theme Support - Apply theme colors */
.rpg-panel[data-theme="sci-fi"] ~ .rpg-music-widget .rpg-music-widget-content,
body[data-theme="sci-fi"] .rpg-music-widget-content {
background: linear-gradient(135deg, #8b00ff 0%, #6a00cc 50%, #5000aa 100%);
}
.rpg-panel[data-theme="sci-fi"] ~ .rpg-music-widget .rpg-music-widget-play,
body[data-theme="sci-fi"] .rpg-music-widget-play {
color: #8b00ff;
}
.rpg-panel[data-theme="fantasy"] ~ .rpg-music-widget .rpg-music-widget-content,
body[data-theme="fantasy"] .rpg-music-widget-content {
background: linear-gradient(135deg, #d4af37 0%, #b8962e 50%, #9c7d25 100%);
}
.rpg-panel[data-theme="fantasy"] ~ .rpg-music-widget .rpg-music-widget-play,
body[data-theme="fantasy"] .rpg-music-widget-play {
color: #d4af37;
}
.rpg-panel[data-theme="cyberpunk"] ~ .rpg-music-widget .rpg-music-widget-content,
body[data-theme="cyberpunk"] .rpg-music-widget-content {
background: linear-gradient(135deg, #ff2a6d 0%, #d91e58 50%, #b31848 100%);
}
.rpg-panel[data-theme="cyberpunk"] ~ .rpg-music-widget .rpg-music-widget-play,
body[data-theme="cyberpunk"] .rpg-music-widget-play {
color: #ff2a6d;
}
/* ============================================
SNOWFLAKES EFFECT
============================================ */
/* Snowflakes container - covers entire viewport */
.rpg-snowflakes-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
overflow: hidden;
}
/* Individual snowflake */
.rpg-snowflake {
position: absolute;
top: -10px;
color: white;
font-size: 1em;
font-family: Arial, sans-serif;
text-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
animation: rpg-snowfall linear infinite;
opacity: 0.8;
}
/* Snowfall animation */
@keyframes rpg-snowfall {
0% {
transform: translateY(0vh) rotate(0deg);
opacity: 0.8;
}
100% {
transform: translateY(100vh) rotate(360deg);
opacity: 0.2;
}
}
/* Create variations for different snowflakes */
.rpg-snowflake:nth-child(2n) {
animation-duration: 12s;
font-size: 0.8em;
}
.rpg-snowflake:nth-child(3n) {
animation-duration: 15s;
font-size: 1.2em;
}
.rpg-snowflake:nth-child(4n) {
animation-duration: 18s;
font-size: 0.6em;
}
.rpg-snowflake:nth-child(5n) {
animation-duration: 10s;
font-size: 1.4em;
}
/* Slower mobile animation for performance */
@media (max-width: 768px) {
.rpg-snowflake {
animation-duration: 20s;
}
}
/* ============================================
HOLIDAY PROMOTION BANNER
============================================ */
/* Mobile-friendly holiday promo */
.rpg-holiday-promo {
max-width: 100%;
word-wrap: break-word;
}
.rpg-holiday-promo a {
display: inline-block;
max-width: 100%;
}
/* Dismiss button hover effect */
#rpg-dismiss-promo:hover {
opacity: 1 !important;
transform: scale(1.1);
}
/* Mobile responsiveness */
@media (max-width: 600px) {
.rpg-holiday-promo {
font-size: 10px !important;
padding: 10px 8px !important;
}
#rpg-dismiss-promo {
font-size: 12px !important;
top: 2px !important;
right: 2px !important;
}
}
/* ============================================
THREE-COLUMN FEATURE TOGGLES LAYOUT
============================================ */
/* Features row container */
.rpg-features-row {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
/* Each feature column */
.rpg-feature-col {
flex: 1;
min-width: 0;
}
.rpg-feature-col .rpg-toggle-label {
justify-content: center;
}
/* Always hide text, show only checkbox + icon */
.rpg-feature-col .rpg-toggle-text {
display: none;
}
.rpg-feature-col .rpg-toggle-label i {
font-size: 1.125rem;
flex-shrink: 0;
}
/* Tablet and below: Hide text, show only checkbox + icon */
@media (max-width: 768px) {
.rpg-feature-col .rpg-toggle-text {
display: none;
}
.rpg-feature-col .rpg-toggle-label i {
font-size: 1.cally with icons only */
@media (max-width: 400px) {
.rpg-features-row {
flex-direction: column;
gap: 0.5rem;
}
.rpg-feature-col .rpg-toggle-label i {
font-size: 1.25rem;
}
}
/* Ensure send_form has relative positioning for music widget placement */
#send_form {
position: relative !important;
}
+75 -8
View File
@@ -10,7 +10,7 @@
<div class="rpg-panel-header">
<h3>
<i class="fa-solid fa-dice-d20"></i>
<span data-i18n-key="template.mainPanel.title">RPG Companion</span>
<span id="rpg-panel-title" data-i18n-key="template.mainPanel.title">RPG Companion</span>
</h3>
</div>
@@ -60,15 +60,44 @@
<div id="rpg-quests" class="rpg-section rpg-quests-section">
<!-- Content will be populated by JavaScript -->
</div>
<!-- Divider after Quests -->
<div id="rpg-divider-quests" class="rpg-divider"></div>
<!-- Music Player Section -->
<div id="rpg-music-player" class="rpg-section rpg-music-section">
<!-- Content will be populated by JavaScript -->
</div>
</div>
<!-- HTML Prompt Toggle -->
<div class="rpg-toggle-container">
<label class="rpg-toggle-label">
<input type="checkbox" id="rpg-toggle-html-prompt">
<i class="fa-solid fa-code"></i>
<span data-i18n-key="template.mainPanel.enableImmersiveHtml">Enable Immersive HTML</span>
</label>
<!-- Feature Toggles Row -->
<div class="rpg-features-row" id="rpg-features-row">
<!-- HTML Prompt Toggle -->
<div class="rpg-toggle-container rpg-feature-col" id="rpg-html-toggle-wrapper">
<label class="rpg-toggle-label" title="Immersive HTML">
<input type="checkbox" id="rpg-toggle-html-prompt">
<i class="fa-solid fa-code"></i>
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.immersiveHtml">Immersive HTML</span>
</label>
</div>
<!-- Spotify Music Toggle -->
<div class="rpg-toggle-container rpg-feature-col" id="rpg-spotify-toggle-wrapper">
<label class="rpg-toggle-label" title="Spotify Music">
<input type="checkbox" id="rpg-toggle-spotify-music">
<i class="fa-brands fa-spotify"></i>
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.spotifyMusic">Spotify Music</span>
</label>
</div>
<!-- Snowflakes Toggle -->
<div class="rpg-toggle-container rpg-feature-col" id="rpg-snowflakes-toggle-wrapper">
<label class="rpg-toggle-label" title="Snowflakes Effect">
<input type="checkbox" id="rpg-toggle-snowflakes">
<i class="fa-solid fa-snowflake"></i>
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.snowflakesEffect">Snowflakes Effect</span>
</label>
</div>
</div>
<!-- Manual Update Button -->
@@ -88,6 +117,15 @@
data-i18n-key="template.mainPanel.settingsButton">Settings</span>
</button>
</div>
<!-- Holiday Promotion -->
<div class="rpg-holiday-promo" id="rpg-holiday-promo" style="text-align: center; padding: 12px 10px; margin-top: 8px; font-size: 11px; opacity: 0.85; position: relative; line-height: 1.5;">
<button id="rpg-dismiss-promo" style="position: absolute; top: 4px; right: 4px; background: none; border: none; color: currentColor; opacity: 0.6; cursor: pointer; padding: 2px 6px; font-size: 14px; line-height: 1;" title="Dismiss permanently"></button>
<div style="margin-bottom: 4px;">Happy Holidays & Happy New Year!</div>
<a href="https://www.electronhub.ai/" target="_blank" style="color: inherit; text-decoration: none; border-bottom: 1px dotted currentColor; display: inline-block;">
🎁 15% OFF for Electron Hub subscriptions with <strong>2026WITHMARINARA</strong> 🎁
</a>
</div>
</div>
</div>
</div>
@@ -240,6 +278,21 @@
Smooth transitions for stats, content updates, and dice rolls
</small>
<label class="checkbox_label">
<input type="checkbox" id="rpg-toggle-show-html-toggle" />
<span data-i18n-key="template.settingsModal.display.showImmersiveHtmlToggle">Show Immersive HTML</span>
</label>
<label class="checkbox_label">
<input type="checkbox" id="rpg-toggle-show-spotify-toggle" />
<span data-i18n-key="template.settingsModal.display.showSpotifyMusicToggle">Show Spotify Music</span>
</label>
<label class="checkbox_label">
<input type="checkbox" id="rpg-toggle-show-snowflakes-toggle" />
<span data-i18n-key="template.settingsModal.display.showSnowflakesToggle">Show Snowflakes Effect</span>
</label>
<label class="checkbox_label">
<input type="checkbox" id="rpg-toggle-plot-buttons" />
<span data-i18n-key="template.settingsModal.display.showPlotProgressionButtons">Show Plot
@@ -666,6 +719,20 @@
</button>
</div>
<!-- Spotify Music Prompt -->
<div class="rpg-prompt-editor-section">
<label for="rpg-prompt-spotify" style="display: block; margin-bottom: 8px; font-weight: 600;">
<i class="fa-brands fa-spotify"></i> Spotify Music Prompt
</label>
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
Injected when "Enable Spotify Music" is enabled. Asks AI to suggest appropriate music for the scene.
</small>
<textarea id="rpg-prompt-spotify" class="rpg-prompt-textarea" rows="4"></textarea>
<button class="menu_button rpg-restore-prompt-btn" data-prompt="spotify" 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;">