Merge pull request #40 from paperboygold/feat/responsive-dashboard-layout
feat: responsive dashboard layout
This commit is contained in:
@@ -27,6 +27,7 @@ import { registerSceneInfoWidget } from './widgets/sceneInfoWidget.js';
|
|||||||
import { registerPresentCharactersWidget } from './widgets/presentCharactersWidget.js';
|
import { registerPresentCharactersWidget } from './widgets/presentCharactersWidget.js';
|
||||||
import { registerInventoryWidget } from './widgets/inventoryWidget.js';
|
import { registerInventoryWidget } from './widgets/inventoryWidget.js';
|
||||||
import { registerQuestsWidget } from './widgets/questsWidget.js';
|
import { registerQuestsWidget } from './widgets/questsWidget.js';
|
||||||
|
import { registerUserSkillsWidget } from './widgets/userSkillsWidget.js';
|
||||||
|
|
||||||
// Global dashboard manager instance
|
// Global dashboard manager instance
|
||||||
let dashboardManager = null;
|
let dashboardManager = null;
|
||||||
@@ -254,6 +255,9 @@ function registerAllWidgets(registry, dependencies) {
|
|||||||
// Quest widget
|
// Quest widget
|
||||||
registerQuestsWidget(registry, dependencies);
|
registerQuestsWidget(registry, dependencies);
|
||||||
|
|
||||||
|
// Skills widget
|
||||||
|
registerUserSkillsWidget(registry, dependencies);
|
||||||
|
|
||||||
console.log(`[RPG Companion] Registered ${registry.getAll().length} widgets`);
|
console.log(`[RPG Companion] Registered ${registry.getAll().length} widgets`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,24 +339,16 @@ function setupDashboardEventListeners(dependencies) {
|
|||||||
if (dashboardManager && dashboardManager.editManager) {
|
if (dashboardManager && dashboardManager.editManager) {
|
||||||
console.log('[RPG Companion] Lock button clicked');
|
console.log('[RPG Companion] Lock button clicked');
|
||||||
dashboardManager.editManager.toggleLock();
|
dashboardManager.editManager.toggleLock();
|
||||||
|
// Refresh header overflow menu to reflect lock button state change
|
||||||
|
if (headerOverflowManager) {
|
||||||
|
setTimeout(() => headerOverflowManager.refresh(), 50);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tracker Settings button (open tracker editor modal)
|
// Tracker Settings button now uses ID 'rpg-open-tracker-editor'
|
||||||
const trackerSettingsBtn = document.querySelector('#rpg-dashboard-tracker-settings');
|
// Event handler is in trackerEditor.js using jQuery delegation
|
||||||
if (trackerSettingsBtn) {
|
|
||||||
trackerSettingsBtn.addEventListener('click', () => {
|
|
||||||
console.log('[RPG Companion] Tracker Settings button clicked');
|
|
||||||
// Trigger the tracker editor button from main UI
|
|
||||||
const trackerEditorBtn = document.getElementById('rpg-open-tracker-editor');
|
|
||||||
if (trackerEditorBtn) {
|
|
||||||
trackerEditorBtn.click();
|
|
||||||
} else {
|
|
||||||
console.warn('[RPG Companion] Tracker editor button not found');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Done button (exit edit mode)
|
// Done button (exit edit mode)
|
||||||
const doneBtn = document.querySelector('#rpg-dashboard-done-edit');
|
const doneBtn = document.querySelector('#rpg-dashboard-done-edit');
|
||||||
|
|||||||
@@ -185,18 +185,8 @@ export class DashboardManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initialize Tab Manager with dashboard data structure
|
// Initialize Tab Manager with dashboard data structure
|
||||||
// Create default tab if no tabs exist
|
// Note: Tabs will be populated by loadLayout() which runs after init()
|
||||||
if (this.dashboard.tabs.length === 0) {
|
// Default layout is set via setDefaultLayout() before init() is called
|
||||||
this.dashboard.tabs.push({
|
|
||||||
id: 'main',
|
|
||||||
name: 'Main',
|
|
||||||
icon: 'fa-solid fa-house',
|
|
||||||
order: 0,
|
|
||||||
widgets: []
|
|
||||||
});
|
|
||||||
this.dashboard.defaultTab = 'main';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tabManager = new TabManager(this.dashboard);
|
this.tabManager = new TabManager(this.dashboard);
|
||||||
|
|
||||||
// Set current tab to active tab from TabManager
|
// Set current tab to active tab from TabManager
|
||||||
@@ -959,7 +949,8 @@ export class DashboardManager {
|
|||||||
scene: [],
|
scene: [],
|
||||||
social: [],
|
social: [],
|
||||||
inventory: [],
|
inventory: [],
|
||||||
quests: []
|
quests: [],
|
||||||
|
skills: []
|
||||||
};
|
};
|
||||||
|
|
||||||
widgets.forEach(widget => {
|
widgets.forEach(widget => {
|
||||||
@@ -1041,6 +1032,19 @@ export class DashboardManager {
|
|||||||
this.gridEngine.autoLayout(groups.quests, { preserveOrder: true });
|
this.gridEngine.autoLayout(groups.quests, { preserveOrder: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create Skills tab if there are skills widgets
|
||||||
|
if (groups.skills.length > 0) {
|
||||||
|
this.dashboard.tabs.push({
|
||||||
|
id: 'tab-skills',
|
||||||
|
name: 'Skills',
|
||||||
|
icon: 'fa-solid fa-book',
|
||||||
|
order: 5,
|
||||||
|
widgets: groups.skills
|
||||||
|
});
|
||||||
|
|
||||||
|
this.gridEngine.autoLayout(groups.skills, { preserveOrder: true });
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[DashboardManager] Created', this.dashboard.tabs.length, 'tabs');
|
console.log('[DashboardManager] Created', this.dashboard.tabs.length, 'tabs');
|
||||||
|
|
||||||
// Re-render tabs and switch to first tab
|
// Re-render tabs and switch to first tab
|
||||||
@@ -1080,7 +1084,8 @@ export class DashboardManager {
|
|||||||
'social': 3,
|
'social': 3,
|
||||||
'inventory': 4,
|
'inventory': 4,
|
||||||
'quests': 5,
|
'quests': 5,
|
||||||
'other': 6
|
'skills': 6,
|
||||||
|
'other': 7
|
||||||
};
|
};
|
||||||
|
|
||||||
// Specific widget type ordering within user category
|
// Specific widget type ordering within user category
|
||||||
@@ -1485,32 +1490,29 @@ export class DashboardManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load saved layout
|
* Load saved layout
|
||||||
|
*
|
||||||
|
* For first-run users (no saved layout), calls resetLayout() for comprehensive
|
||||||
|
* initialization. This ensures consistent behavior between first-run and manual
|
||||||
|
* reset, using a single code path for default layout setup.
|
||||||
*/
|
*/
|
||||||
async loadLayout() {
|
async loadLayout() {
|
||||||
try {
|
try {
|
||||||
const saved = await this.persistence.loadLayout();
|
const saved = await this.persistence.loadLayout();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
|
console.log('[DashboardManager] Loading saved layout');
|
||||||
this.applyDashboardConfig(saved);
|
this.applyDashboardConfig(saved);
|
||||||
} else if (this.defaultLayout) {
|
} else {
|
||||||
console.log('[DashboardManager] No saved layout, using default with auto-layout');
|
// First run - use resetLayout() for comprehensive initialization
|
||||||
this.applyDashboardConfig(this.defaultLayout);
|
// This provides: fresh layout generation, state reset, validation,
|
||||||
|
// column-aware sizing, and proper UI rendering
|
||||||
// Auto-layout each tab to prevent overlap (default positions may not fit screen)
|
console.log('[DashboardManager] No saved layout found, calling resetLayout() for first-run initialization');
|
||||||
this.dashboard.tabs.forEach(tab => {
|
await this.resetLayout();
|
||||||
if (tab.widgets && tab.widgets.length > 0) {
|
|
||||||
console.log(`[DashboardManager] Auto-laying out default tab "${tab.name}" (${tab.widgets.length} widgets)`);
|
|
||||||
this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save the auto-laid-out default as the initial saved layout
|
|
||||||
await this.saveLayout(true);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[DashboardManager] Failed to load layout:', error);
|
console.error('[DashboardManager] Failed to load layout:', error);
|
||||||
if (this.defaultLayout) {
|
// Fallback: use resetLayout() for clean state recovery
|
||||||
this.applyDashboardConfig(this.defaultLayout);
|
console.log('[DashboardManager] Recovering with resetLayout()');
|
||||||
}
|
await this.resetLayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1562,7 +1564,8 @@ export class DashboardManager {
|
|||||||
// Skip initial switch in applyDashboardConfig since we'll switch after layout calculations
|
// Skip initial switch in applyDashboardConfig since we'll switch after layout calculations
|
||||||
this.applyDashboardConfig(this.defaultLayout, { skipInitialSwitch: true });
|
this.applyDashboardConfig(this.defaultLayout, { skipInitialSwitch: true });
|
||||||
|
|
||||||
// Reset all widgets to default sizes
|
// Apply column-aware widget sizes from widget definitions
|
||||||
|
// This makes widgets scale properly based on screen width (2-4 columns)
|
||||||
const allWidgets = [];
|
const allWidgets = [];
|
||||||
this.dashboard.tabs.forEach(tab => {
|
this.dashboard.tabs.forEach(tab => {
|
||||||
if (tab.widgets && tab.widgets.length > 0) {
|
if (tab.widgets && tab.widgets.length > 0) {
|
||||||
@@ -1571,13 +1574,10 @@ export class DashboardManager {
|
|||||||
});
|
});
|
||||||
this.resetWidgetSizesToDefault(allWidgets);
|
this.resetWidgetSizesToDefault(allWidgets);
|
||||||
|
|
||||||
// Auto-layout each tab to prevent overlap (default positions may have changed)
|
// Don't call autoLayout - preserve positions from defaultLayout.js
|
||||||
this.dashboard.tabs.forEach(tab => {
|
// Widget definitions now have column-aware sizes (defaultSize returns correct size for column count)
|
||||||
if (tab.widgets && tab.widgets.length > 0) {
|
// ResizeObserver will handle column changes and trigger autoLayout when screen resizes
|
||||||
console.log(`[DashboardManager] Auto-laying out tab "${tab.name}" (${tab.widgets.length} widgets)`);
|
console.log('[DashboardManager] Using column-aware sizes from widget definitions, preserving positions from defaultLayout.js');
|
||||||
this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Force re-render tabs
|
// Force re-render tabs
|
||||||
this.renderTabs();
|
this.renderTabs();
|
||||||
@@ -1631,6 +1631,146 @@ export class DashboardManager {
|
|||||||
console.log(`[DashboardManager] Reset ${resetCount} widgets to default sizes`);
|
console.log(`[DashboardManager] Reset ${resetCount} widgets to default sizes`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to apply default layout positions to current tab
|
||||||
|
*
|
||||||
|
* Checks if the current tab's widgets match the default layout and applies
|
||||||
|
* the default positions if they do. This ensures "Sort Current Page" produces
|
||||||
|
* the same layout as "Reset Layout" for default widgets.
|
||||||
|
*
|
||||||
|
* @param {Object} tab - Tab to apply default layout to
|
||||||
|
* @param {Object} options - Layout options
|
||||||
|
* @returns {boolean} True if default layout was applied, false otherwise
|
||||||
|
*/
|
||||||
|
tryApplyDefaultLayoutToTab(tab, options = {}) {
|
||||||
|
if (!this.defaultLayout || !this.defaultLayout.tabs) {
|
||||||
|
console.log('[DashboardManager] No default layout available');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching default tab by ID
|
||||||
|
const defaultTab = this.defaultLayout.tabs.find(t => t.id === tab.id);
|
||||||
|
if (!defaultTab) {
|
||||||
|
console.log(`[DashboardManager] No default layout for tab "${tab.name}" (${tab.id})`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if widgets match (same types, possibly different IDs)
|
||||||
|
const currentTypes = tab.widgets.map(w => w.type).sort();
|
||||||
|
const defaultTypes = defaultTab.widgets.map(w => w.type).sort();
|
||||||
|
|
||||||
|
if (currentTypes.length !== defaultTypes.length ||
|
||||||
|
!currentTypes.every((type, i) => type === defaultTypes[i])) {
|
||||||
|
console.log('[DashboardManager] Tab widgets do not match default layout (custom widgets present)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DashboardManager] Applying default layout positions to current tab');
|
||||||
|
|
||||||
|
// Reset widget sizes to defaults (unless explicitly disabled)
|
||||||
|
if (options.resetSizes !== false) {
|
||||||
|
this.resetWidgetSizesToDefault(tab.widgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply default positions to each widget
|
||||||
|
tab.widgets.forEach(widget => {
|
||||||
|
const defaultWidget = defaultTab.widgets.find(w => w.type === widget.type);
|
||||||
|
if (defaultWidget) {
|
||||||
|
widget.x = defaultWidget.x;
|
||||||
|
widget.y = defaultWidget.y;
|
||||||
|
// Size is already set by resetWidgetSizesToDefault
|
||||||
|
console.log(`[DashboardManager] Set ${widget.type} to default position (${widget.x}, ${widget.y})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to apply default layout to all tabs
|
||||||
|
*
|
||||||
|
* Checks if the current dashboard widgets match the default layout and applies
|
||||||
|
* the default positions if they do. This ensures "Auto Arrange" produces
|
||||||
|
* the same layout as "Reset Layout" for default widgets.
|
||||||
|
*
|
||||||
|
* @param {Object} options - Layout options
|
||||||
|
* @returns {boolean} True if default layout was applied, false otherwise
|
||||||
|
*/
|
||||||
|
tryApplyDefaultLayout(options = {}) {
|
||||||
|
if (!this.defaultLayout || !this.defaultLayout.tabs) {
|
||||||
|
console.log('[DashboardManager] No default layout available');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tabs match default layout
|
||||||
|
if (this.dashboard.tabs.length !== this.defaultLayout.tabs.length) {
|
||||||
|
console.log('[DashboardManager] Tab count does not match default layout');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all tabs and widgets match
|
||||||
|
for (let i = 0; i < this.dashboard.tabs.length; i++) {
|
||||||
|
const tab = this.dashboard.tabs[i];
|
||||||
|
const defaultTab = this.defaultLayout.tabs.find(t => t.id === tab.id);
|
||||||
|
|
||||||
|
if (!defaultTab) {
|
||||||
|
console.log(`[DashboardManager] No default tab found for "${tab.name}" (${tab.id})`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTypes = tab.widgets.map(w => w.type).sort();
|
||||||
|
const defaultTypes = defaultTab.widgets.map(w => w.type).sort();
|
||||||
|
|
||||||
|
if (currentTypes.length !== defaultTypes.length ||
|
||||||
|
!currentTypes.every((type, j) => type === defaultTypes[j])) {
|
||||||
|
console.log(`[DashboardManager] Tab "${tab.name}" widgets do not match default layout`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DashboardManager] Applying default layout positions to all tabs');
|
||||||
|
|
||||||
|
// Gather all widgets from all tabs
|
||||||
|
const allWidgets = [];
|
||||||
|
this.dashboard.tabs.forEach(tab => {
|
||||||
|
if (tab.widgets && tab.widgets.length > 0) {
|
||||||
|
allWidgets.push(...tab.widgets);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset widget sizes to defaults (unless explicitly disabled)
|
||||||
|
if (options.resetSizes !== false) {
|
||||||
|
this.resetWidgetSizesToDefault(allWidgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply default positions to each tab
|
||||||
|
this.dashboard.tabs.forEach(tab => {
|
||||||
|
const defaultTab = this.defaultLayout.tabs.find(t => t.id === tab.id);
|
||||||
|
if (defaultTab) {
|
||||||
|
tab.widgets.forEach(widget => {
|
||||||
|
const defaultWidget = defaultTab.widgets.find(w => w.type === widget.type);
|
||||||
|
if (defaultWidget) {
|
||||||
|
widget.x = defaultWidget.x;
|
||||||
|
widget.y = defaultWidget.y;
|
||||||
|
console.log(`[DashboardManager] Set ${widget.type} to default position (${widget.x}, ${widget.y})`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-render tabs and switch to first tab
|
||||||
|
this.renderTabs();
|
||||||
|
if (this.dashboard.tabs.length > 0) {
|
||||||
|
this.switchTab(this.dashboard.tabs[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save layout
|
||||||
|
this.triggerAutoSave();
|
||||||
|
|
||||||
|
console.log('[DashboardManager] Default layout applied successfully');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-layout widgets on current tab only
|
* Auto-layout widgets on current tab only
|
||||||
* Sorts and arranges widgets on the current tab to maximize space usage
|
* Sorts and arranges widgets on the current tab to maximize space usage
|
||||||
@@ -1656,40 +1796,48 @@ export class DashboardManager {
|
|||||||
|
|
||||||
console.log(`[DashboardManager] Laying out ${currentTab.widgets.length} widgets on tab "${currentTab.name}"`);
|
console.log(`[DashboardManager] Laying out ${currentTab.widgets.length} widgets on tab "${currentTab.name}"`);
|
||||||
|
|
||||||
// Reset widget sizes to defaults (unless explicitly disabled)
|
// Check if we can use default layout positions
|
||||||
if (options.resetSizes !== false) {
|
const useDefaultLayout = this.tryApplyDefaultLayoutToTab(currentTab, options);
|
||||||
this.resetWidgetSizesToDefault(currentTab.widgets);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort widgets by category for better organization
|
if (!useDefaultLayout) {
|
||||||
const sortedWidgets = this.sortWidgetsByCategory(currentTab.widgets);
|
// Fallback to traditional auto-layout
|
||||||
|
console.log('[DashboardManager] Using gridEngine.autoLayout (custom widgets or no default layout)');
|
||||||
|
|
||||||
// Update tab's widgets array with sorted order
|
// Reset widget sizes to defaults (unless explicitly disabled)
|
||||||
currentTab.widgets = sortedWidgets;
|
if (options.resetSizes !== false) {
|
||||||
|
this.resetWidgetSizesToDefault(currentTab.widgets);
|
||||||
// Store current widget dimensions before auto-layout
|
|
||||||
const dimensionsBefore = new Map();
|
|
||||||
currentTab.widgets.forEach(widget => {
|
|
||||||
dimensionsBefore.set(widget.id, { w: widget.w, h: widget.h });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-layout widgets on the current tab
|
|
||||||
this.gridEngine.autoLayout(currentTab.widgets, {
|
|
||||||
preserveOrder: options.preserveOrder !== false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call onResize handlers for widgets whose dimensions changed
|
|
||||||
// This allows widgets to update internal layouts (e.g., User Attributes grid columns)
|
|
||||||
currentTab.widgets.forEach(widget => {
|
|
||||||
const before = dimensionsBefore.get(widget.id);
|
|
||||||
if (before && (before.w !== widget.w || before.h !== widget.h)) {
|
|
||||||
const widgetData = this.widgets.get(widget.id);
|
|
||||||
if (widgetData?.definition?.onResize && widgetData.element) {
|
|
||||||
console.log(`[DashboardManager] Calling onResize for ${widget.type} (${before.w}x${before.h} → ${widget.w}x${widget.h})`);
|
|
||||||
widgetData.definition.onResize(widgetData.element, widget.w, widget.h);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Sort widgets by category for better organization
|
||||||
|
const sortedWidgets = this.sortWidgetsByCategory(currentTab.widgets);
|
||||||
|
|
||||||
|
// Update tab's widgets array with sorted order
|
||||||
|
currentTab.widgets = sortedWidgets;
|
||||||
|
|
||||||
|
// Store current widget dimensions before auto-layout
|
||||||
|
const dimensionsBefore = new Map();
|
||||||
|
currentTab.widgets.forEach(widget => {
|
||||||
|
dimensionsBefore.set(widget.id, { w: widget.w, h: widget.h });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-layout widgets on the current tab
|
||||||
|
this.gridEngine.autoLayout(currentTab.widgets, {
|
||||||
|
preserveOrder: options.preserveOrder !== false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call onResize handlers for widgets whose dimensions changed
|
||||||
|
// This allows widgets to update internal layouts (e.g., User Attributes grid columns)
|
||||||
|
currentTab.widgets.forEach(widget => {
|
||||||
|
const before = dimensionsBefore.get(widget.id);
|
||||||
|
if (before && (before.w !== widget.w || before.h !== widget.h)) {
|
||||||
|
const widgetData = this.widgets.get(widget.id);
|
||||||
|
if (widgetData?.definition?.onResize && widgetData.element) {
|
||||||
|
console.log(`[DashboardManager] Calling onResize for ${widget.type} (${before.w}x${before.h} → ${widget.w}x${widget.h})`);
|
||||||
|
widgetData.definition.onResize(widgetData.element, widget.w, widget.h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Re-render all widgets with new positions
|
// Re-render all widgets with new positions
|
||||||
this.clearGrid();
|
this.clearGrid();
|
||||||
@@ -1721,42 +1869,50 @@ export class DashboardManager {
|
|||||||
console.log('[DashboardManager] ===== AUTO-LAYOUT WIDGETS CALLED =====');
|
console.log('[DashboardManager] ===== AUTO-LAYOUT WIDGETS CALLED =====');
|
||||||
console.log('[DashboardManager] Auto-layout widgets requested');
|
console.log('[DashboardManager] Auto-layout widgets requested');
|
||||||
|
|
||||||
// Gather ALL widgets from ALL tabs (don't lose inventory, social, etc.)
|
// Check if we can use default layout
|
||||||
const allWidgets = [];
|
const useDefaultLayout = this.tryApplyDefaultLayout(options);
|
||||||
this.dashboard.tabs.forEach(tab => {
|
|
||||||
if (tab.widgets && tab.widgets.length > 0) {
|
if (!useDefaultLayout) {
|
||||||
console.log(`[DashboardManager] Gathering ${tab.widgets.length} widgets from tab "${tab.name}"`);
|
// Fallback to traditional auto-layout
|
||||||
allWidgets.push(...tab.widgets);
|
console.log('[DashboardManager] Using traditional auto-layout (custom widgets or no default layout)');
|
||||||
|
|
||||||
|
// Gather ALL widgets from ALL tabs (don't lose inventory, social, etc.)
|
||||||
|
const allWidgets = [];
|
||||||
|
this.dashboard.tabs.forEach(tab => {
|
||||||
|
if (tab.widgets && tab.widgets.length > 0) {
|
||||||
|
console.log(`[DashboardManager] Gathering ${tab.widgets.length} widgets from tab "${tab.name}"`);
|
||||||
|
allWidgets.push(...tab.widgets);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allWidgets.length === 0) {
|
||||||
|
console.warn('[DashboardManager] No widgets to auto-layout');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (allWidgets.length === 0) {
|
console.log(`[DashboardManager] Total widgets to layout: ${allWidgets.length}`);
|
||||||
console.warn('[DashboardManager] No widgets to auto-layout');
|
|
||||||
return;
|
// Reset widget sizes to defaults (unless explicitly disabled)
|
||||||
|
if (options.resetSizes !== false) {
|
||||||
|
this.resetWidgetSizesToDefault(allWidgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smart category-aware sorting BEFORE auto-layout
|
||||||
|
const widgetsToLayout = this.sortWidgetsByCategory(allWidgets);
|
||||||
|
|
||||||
|
// Calculate estimated height to determine if multi-tab distribution is needed
|
||||||
|
const estimatedHeight = this.estimateLayoutHeight(widgetsToLayout);
|
||||||
|
const heightThreshold = 80; // rem - reasonable max height for single tab
|
||||||
|
|
||||||
|
console.log('[DashboardManager] Estimated height:', estimatedHeight + 'rem', 'Threshold:', heightThreshold + 'rem');
|
||||||
|
|
||||||
|
// Always use multi-tab distribution when we have many widgets
|
||||||
|
// This preserves all widgets (inventory, social, etc.)
|
||||||
|
console.log('[DashboardManager] Using multi-tab distribution to preserve all widgets');
|
||||||
|
this.distributeWidgetsByCategory(widgetsToLayout);
|
||||||
|
|
||||||
|
// distributeWidgetsByCategory handles rendering and tab switching
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DashboardManager] Total widgets to layout: ${allWidgets.length}`);
|
|
||||||
|
|
||||||
// Reset widget sizes to defaults (unless explicitly disabled)
|
|
||||||
if (options.resetSizes !== false) {
|
|
||||||
this.resetWidgetSizesToDefault(allWidgets);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smart category-aware sorting BEFORE auto-layout
|
|
||||||
const widgetsToLayout = this.sortWidgetsByCategory(allWidgets);
|
|
||||||
|
|
||||||
// Calculate estimated height to determine if multi-tab distribution is needed
|
|
||||||
const estimatedHeight = this.estimateLayoutHeight(widgetsToLayout);
|
|
||||||
const heightThreshold = 80; // rem - reasonable max height for single tab
|
|
||||||
|
|
||||||
console.log('[DashboardManager] Estimated height:', estimatedHeight + 'rem', 'Threshold:', heightThreshold + 'rem');
|
|
||||||
|
|
||||||
// Always use multi-tab distribution when we have many widgets
|
|
||||||
// This preserves all widgets (inventory, social, etc.)
|
|
||||||
console.log('[DashboardManager] Using multi-tab distribution to preserve all widgets');
|
|
||||||
this.distributeWidgetsByCategory(widgetsToLayout);
|
|
||||||
|
|
||||||
// distributeWidgetsByCategory handles rendering and tab switching
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button id="rpg-dashboard-tracker-settings" class="rpg-dashboard-btn rpg-tracker-settings-btn rpg-priority-btn" title="Tracker Settings - Customize fields, names, and AI instructions" aria-label="Tracker settings">
|
<button id="rpg-open-tracker-editor" class="rpg-dashboard-btn rpg-tracker-settings-btn rpg-priority-btn" title="Tracker Settings" aria-label="Tracker settings">
|
||||||
<i class="fa-solid fa-sliders"></i>
|
<i class="fa-solid fa-sliders"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -39,45 +39,46 @@ export function generateDefaultDashboard() {
|
|||||||
icon: 'fa-solid fa-user',
|
icon: 'fa-solid fa-user',
|
||||||
order: 0,
|
order: 0,
|
||||||
widgets: [
|
widgets: [
|
||||||
// Row 0: User Info (left) + User Mood (top right in 3-col)
|
// Row 0-1: User Info (left column, vertical)
|
||||||
{
|
{
|
||||||
id: 'widget-userinfo',
|
id: 'widget-userinfo',
|
||||||
type: 'userInfo',
|
type: 'userInfo',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
w: 2,
|
|
||||||
h: 1,
|
|
||||||
config: {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'widget-usermood',
|
|
||||||
type: 'userMood',
|
|
||||||
x: 2,
|
|
||||||
y: 0,
|
|
||||||
w: 1,
|
w: 1,
|
||||||
h: 1,
|
h: 2,
|
||||||
config: {}
|
config: {}
|
||||||
},
|
},
|
||||||
// Row 1-2: User Stats (health/energy bars)
|
// Row 0-2: User Stats (right side, tall, 2 cols wide)
|
||||||
{
|
{
|
||||||
id: 'widget-userstats',
|
id: 'widget-userstats',
|
||||||
type: 'userStats',
|
type: 'userStats',
|
||||||
x: 0,
|
x: 1,
|
||||||
y: 1,
|
y: 0,
|
||||||
w: 2,
|
w: 2,
|
||||||
h: 2,
|
h: 3,
|
||||||
config: {
|
config: {
|
||||||
statBarGradient: true
|
statBarGradient: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Row 3-4: User Attributes
|
// Row 2: User Mood (below user info, left column)
|
||||||
|
{
|
||||||
|
id: 'widget-usermood',
|
||||||
|
type: 'userMood',
|
||||||
|
x: 0,
|
||||||
|
y: 2,
|
||||||
|
w: 1,
|
||||||
|
h: 1,
|
||||||
|
config: {}
|
||||||
|
},
|
||||||
|
// Row 3-6: User Attributes (full width below everything, 3 cols wide)
|
||||||
{
|
{
|
||||||
id: 'widget-userattributes',
|
id: 'widget-userattributes',
|
||||||
type: 'userAttributes',
|
type: 'userAttributes',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 3,
|
y: 3,
|
||||||
w: 2,
|
w: 3,
|
||||||
h: 2,
|
h: 4,
|
||||||
config: {}
|
config: {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -89,36 +90,36 @@ export function generateDefaultDashboard() {
|
|||||||
icon: 'fa-solid fa-map',
|
icon: 'fa-solid fa-map',
|
||||||
order: 1,
|
order: 1,
|
||||||
widgets: [
|
widgets: [
|
||||||
// Row 0-1: Scene Info (combined: calendar, weather, temp, clock, location)
|
// Row 0-2: Scene Info (combined: calendar, weather, temp, clock, location)
|
||||||
{
|
{
|
||||||
id: 'widget-sceneinfo',
|
id: 'widget-sceneinfo',
|
||||||
type: 'sceneInfo',
|
type: 'sceneInfo',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
w: 2,
|
w: 3,
|
||||||
h: 2,
|
h: 3,
|
||||||
config: {}
|
config: {}
|
||||||
},
|
},
|
||||||
// Row 2-3: Recent Events (notebook style, full width)
|
// Row 3-4: Recent Events (notebook style, full width)
|
||||||
{
|
{
|
||||||
id: 'widget-recentevents',
|
id: 'widget-recentevents',
|
||||||
type: 'recentEvents',
|
type: 'recentEvents',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 2,
|
y: 3,
|
||||||
w: 2,
|
w: 3,
|
||||||
h: 2,
|
h: 2,
|
||||||
config: {
|
config: {
|
||||||
maxEvents: 3
|
maxEvents: 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Row 4-7: Present Characters (full width, will expand with auto-layout)
|
// Row 5-6: Present Characters (full width, fits 1080p screen)
|
||||||
{
|
{
|
||||||
id: 'widget-presentchars',
|
id: 'widget-presentchars',
|
||||||
type: 'presentCharacters',
|
type: 'presentCharacters',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 4,
|
y: 5,
|
||||||
w: 2,
|
w: 3,
|
||||||
h: 4,
|
h: 2,
|
||||||
config: {
|
config: {
|
||||||
cardLayout: 'grid',
|
cardLayout: 'grid',
|
||||||
showThoughtBubbles: true
|
showThoughtBubbles: true
|
||||||
@@ -166,6 +167,29 @@ export function generateDefaultDashboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
// Tab 5: Skills (Full tab for skills system)
|
||||||
|
{
|
||||||
|
id: 'tab-skills',
|
||||||
|
name: 'Skills',
|
||||||
|
icon: 'fa-solid fa-book',
|
||||||
|
order: 4,
|
||||||
|
widgets: [
|
||||||
|
{
|
||||||
|
id: 'widget-userskills',
|
||||||
|
type: 'userSkills',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 3,
|
||||||
|
h: 7,
|
||||||
|
config: {
|
||||||
|
defaultSubTab: 'all',
|
||||||
|
showXP: true,
|
||||||
|
showCategories: true,
|
||||||
|
maxLevel: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -336,15 +336,6 @@ export class EditModeManager {
|
|||||||
controls.style.opacity = '0';
|
controls.style.opacity = '0';
|
||||||
controls.style.transition = 'opacity 0.2s';
|
controls.style.transition = 'opacity 0.2s';
|
||||||
|
|
||||||
// Settings button
|
|
||||||
const settingsBtn = this.createControlButton('⚙', 'Settings');
|
|
||||||
settingsBtn.onclick = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (this.onWidgetSettings) {
|
|
||||||
this.onWidgetSettings(widgetId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete button
|
// Delete button
|
||||||
const deleteBtn = this.createControlButton('×', 'Delete');
|
const deleteBtn = this.createControlButton('×', 'Delete');
|
||||||
deleteBtn.onclick = (e) => {
|
deleteBtn.onclick = (e) => {
|
||||||
@@ -353,7 +344,6 @@ export class EditModeManager {
|
|||||||
};
|
};
|
||||||
deleteBtn.style.background = '#e94560';
|
deleteBtn.style.background = '#e94560';
|
||||||
|
|
||||||
controls.appendChild(settingsBtn);
|
|
||||||
controls.appendChild(deleteBtn);
|
controls.appendChild(deleteBtn);
|
||||||
|
|
||||||
// Store reference to widget element for positioning
|
// Store reference to widget element for positioning
|
||||||
|
|||||||
@@ -529,7 +529,19 @@ export function registerRecentEventsWidget(registry, dependencies) {
|
|||||||
description: 'Recent events notebook',
|
description: 'Recent events notebook',
|
||||||
category: 'scene',
|
category: 'scene',
|
||||||
minSize: { w: 2, h: 2 },
|
minSize: { w: 2, h: 2 },
|
||||||
defaultSize: { w: 2, h: 2 },
|
// Column-aware sizing: full width at all sizes
|
||||||
|
defaultSize: (columns) => {
|
||||||
|
if (columns <= 2) {
|
||||||
|
return { w: 2, h: 2 }; // Mobile: 2 cols wide (full), 2 rows
|
||||||
|
}
|
||||||
|
return { w: 3, h: 2 }; // Desktop: 3 cols wide (full), 2 rows
|
||||||
|
},
|
||||||
|
maxAutoSize: (columns) => {
|
||||||
|
if (columns <= 2) {
|
||||||
|
return { w: 2, h: 3 };
|
||||||
|
}
|
||||||
|
return { w: 3, h: 3 };
|
||||||
|
},
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -538,7 +550,40 @@ export function registerRecentEventsWidget(registry, dependencies) {
|
|||||||
* @param {Object} config - Widget configuration
|
* @param {Object} config - Widget configuration
|
||||||
*/
|
*/
|
||||||
render(container, config = {}) {
|
render(container, config = {}) {
|
||||||
const { getInfoBoxData } = dependencies;
|
const { getInfoBoxData, getExtensionSettings } = dependencies;
|
||||||
|
|
||||||
|
// Check if Recent Events is enabled in tracker config
|
||||||
|
const settings = getExtensionSettings();
|
||||||
|
const trackerConfig = settings.trackerConfig;
|
||||||
|
const isEnabled = trackerConfig?.infoBox?.widgets?.recentEvents?.enabled !== false;
|
||||||
|
|
||||||
|
// If disabled, show helpful message
|
||||||
|
if (!isEnabled) {
|
||||||
|
const html = `
|
||||||
|
<div class="rpg-dashboard-widget">
|
||||||
|
<div class="rpg-events-widget rpg-widget-disabled">
|
||||||
|
<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-widget-disabled-message">
|
||||||
|
<i class="fa-solid fa-circle-info"></i>
|
||||||
|
<p>Recent Events tracking is currently disabled.</p>
|
||||||
|
<button class="rpg-widget-enable-btn" data-widget-type="recentEvents">
|
||||||
|
<i class="fa-solid fa-toggle-on"></i>
|
||||||
|
Enable in Tracker Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.innerHTML = html;
|
||||||
|
attachDisabledStateHandlers(container);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = parseInfoBoxData(getInfoBoxData());
|
const data = parseInfoBoxData(getInfoBoxData());
|
||||||
|
|
||||||
// Merge default config with user config
|
// Merge default config with user config
|
||||||
@@ -755,3 +800,31 @@ function updateRecentEvent(eventIndex, value, dependencies) {
|
|||||||
|
|
||||||
console.log(`[Recent Events Widget] Updated event ${eventIndex}: "${value}"`);
|
console.log(`[Recent Events Widget] Updated event ${eventIndex}: "${value}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach handlers for disabled widget state
|
||||||
|
* Opens Tracker Settings when "Enable" button is clicked
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function attachDisabledStateHandlers(container) {
|
||||||
|
const enableBtn = container.querySelector('.rpg-widget-enable-btn');
|
||||||
|
if (enableBtn) {
|
||||||
|
enableBtn.addEventListener('click', () => {
|
||||||
|
// Open Tracker Settings modal
|
||||||
|
const trackerSettingsBtn = document.querySelector('#rpg-open-tracker-editor');
|
||||||
|
if (trackerSettingsBtn) {
|
||||||
|
trackerSettingsBtn.click();
|
||||||
|
|
||||||
|
// After modal opens, switch to Info Box tab
|
||||||
|
setTimeout(() => {
|
||||||
|
const infoBoxTab = document.querySelector('.rpg-editor-tab[data-tab="infoBox"]');
|
||||||
|
if (infoBoxTab) {
|
||||||
|
infoBoxTab.click();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
console.warn('[Recent Events Widget] Tracker Settings button not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,18 +65,18 @@ export function registerInventoryWidget(registry, dependencies) {
|
|||||||
description: 'Full inventory system with On Person, Stored, and Assets',
|
description: 'Full inventory system with On Person, Stored, and Assets',
|
||||||
category: 'inventory',
|
category: 'inventory',
|
||||||
minSize: { w: 2, h: 4 },
|
minSize: { w: 2, h: 4 },
|
||||||
// Column-aware sizing: compact on mobile, spacious on desktop
|
// Column-aware sizing: compact on mobile, full width on desktop
|
||||||
defaultSize: (columns) => {
|
defaultSize: (columns) => {
|
||||||
if (columns <= 2) {
|
if (columns <= 2) {
|
||||||
return { w: 2, h: 5 }; // Mobile: 2×5 (full width, compact)
|
return { w: 2, h: 5 }; // Mobile: 2×5 (full width, compact)
|
||||||
}
|
}
|
||||||
return { w: 2, h: 6 }; // Desktop: 2×6 (default)
|
return { w: 3, h: 7 }; // Desktop: 3×7 (full width, spacious for 1080p)
|
||||||
},
|
},
|
||||||
maxAutoSize: (columns) => {
|
maxAutoSize: (columns) => {
|
||||||
if (columns <= 2) {
|
if (columns <= 2) {
|
||||||
return { w: 2, h: 8 }; // Mobile: 2×8 max (increased for expansion headroom)
|
return { w: 2, h: 8 }; // Mobile: 2×8 max
|
||||||
}
|
}
|
||||||
return { w: 3, h: 8 }; // Desktop: 3×8 max (can expand)
|
return { w: 3, h: 10 }; // Desktop: 3×10 max (can expand)
|
||||||
},
|
},
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|
||||||
|
|||||||
@@ -276,8 +276,20 @@ export function registerPresentCharactersWidget(registry, dependencies) {
|
|||||||
description: 'Character cards with avatars, traits, and relationships',
|
description: 'Character cards with avatars, traits, and relationships',
|
||||||
category: 'scene',
|
category: 'scene',
|
||||||
minSize: { w: 2, h: 2 },
|
minSize: { w: 2, h: 2 },
|
||||||
defaultSize: { w: 2, h: 2 }, // Compact size fits both mobile and desktop viewports
|
// Column-aware sizing: narrow and tall on mobile, wide and short on desktop
|
||||||
maxAutoSize: { w: 4, h: 5 }, // Max size for auto-arrange expansion (supports up to 4-col on large displays)
|
defaultSize: (columns) => {
|
||||||
|
if (columns <= 2) {
|
||||||
|
return { w: 2, h: 4 }; // Mobile: 2 cols wide (full), 4 rows tall
|
||||||
|
}
|
||||||
|
return { w: 3, h: 2 }; // Desktop: 3 cols wide (full), 2 rows tall (fits 1080p)
|
||||||
|
},
|
||||||
|
// Column-aware max size: same as default to prevent expansion
|
||||||
|
maxAutoSize: (columns) => {
|
||||||
|
if (columns <= 2) {
|
||||||
|
return { w: 2, h: 4 }; // Mobile: stay at 4 rows
|
||||||
|
}
|
||||||
|
return { w: 3, h: 2 }; // Desktop: stay at 2 rows (fits 1080p without scrolling)
|
||||||
|
},
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|
||||||
render(container, config = {}) {
|
render(container, config = {}) {
|
||||||
|
|||||||
@@ -395,18 +395,18 @@ export function registerQuestsWidget(registry, dependencies) {
|
|||||||
description: 'Quest tracking with main and optional quests',
|
description: 'Quest tracking with main and optional quests',
|
||||||
category: 'quests',
|
category: 'quests',
|
||||||
minSize: { w: 2, h: 4 },
|
minSize: { w: 2, h: 4 },
|
||||||
// Column-aware sizing: compact on mobile, spacious on desktop
|
// Column-aware sizing: compact on mobile, full width on desktop
|
||||||
defaultSize: (columns) => {
|
defaultSize: (columns) => {
|
||||||
if (columns <= 2) {
|
if (columns <= 2) {
|
||||||
return { w: 2, h: 4 }; // Mobile: 2×4 (full width, compact)
|
return { w: 2, h: 5 }; // Mobile: 2×5 (full width, compact)
|
||||||
}
|
}
|
||||||
return { w: 2, h: 5 }; // Desktop: 2×5 (default)
|
return { w: 3, h: 7 }; // Desktop: 3×7 (full width, spacious for 1080p)
|
||||||
},
|
},
|
||||||
maxAutoSize: (columns) => {
|
maxAutoSize: (columns) => {
|
||||||
if (columns <= 2) {
|
if (columns <= 2) {
|
||||||
return { w: 2, h: 7 }; // Mobile: 2×7 max (increased for expansion headroom)
|
return { w: 2, h: 8 }; // Mobile: 2×8 max
|
||||||
}
|
}
|
||||||
return { w: 3, h: 7 }; // Desktop: 3×7 max (can expand)
|
return { w: 3, h: 10 }; // Desktop: 3×10 max (can expand)
|
||||||
},
|
},
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,20 @@ export function registerUserAttributesWidget(registry, dependencies) {
|
|||||||
description: 'Customizable RPG attributes with +/- buttons (STR, DEX, etc.)',
|
description: 'Customizable RPG attributes with +/- buttons (STR, DEX, etc.)',
|
||||||
category: 'user',
|
category: 'user',
|
||||||
minSize: { w: 2, h: 2 },
|
minSize: { w: 2, h: 2 },
|
||||||
defaultSize: { w: 2, h: 2 },
|
// Column-aware sizing: full width at each column count
|
||||||
maxAutoSize: { w: 3, h: 5 }, // Max size for auto-arrange expansion
|
defaultSize: (columns) => {
|
||||||
|
if (columns <= 2) {
|
||||||
|
return { w: 2, h: 4 }; // Mobile: 2 cols wide (full), 4 rows tall
|
||||||
|
}
|
||||||
|
return { w: 3, h: 4 }; // Desktop: 3 cols wide (full), 4 rows tall
|
||||||
|
},
|
||||||
|
// Column-aware max size: same as default
|
||||||
|
maxAutoSize: (columns) => {
|
||||||
|
if (columns <= 2) {
|
||||||
|
return { w: 2, h: 4 };
|
||||||
|
}
|
||||||
|
return { w: 3, h: 4 };
|
||||||
|
},
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -38,19 +38,22 @@ export function registerUserInfoWidget(registry, dependencies) {
|
|||||||
description: 'User avatar, name, and level display',
|
description: 'User avatar, name, and level display',
|
||||||
category: 'user',
|
category: 'user',
|
||||||
minSize: { w: 1, h: 1 },
|
minSize: { w: 1, h: 1 },
|
||||||
// Column-aware default size: start at 2x1 in desktop so mood doesn't block expansion
|
// Column-aware default size: vertical 1x2 with mood below
|
||||||
defaultSize: (columns) => {
|
defaultSize: (columns) => {
|
||||||
if (columns <= 2) {
|
// Mobile detection: screen width ≤ 1000px uses compact 1x1
|
||||||
return { w: 1, h: 1 }; // Mobile: 1x1, horizontal layout
|
const isMobile = window.innerWidth <= 1000;
|
||||||
|
if (isMobile) {
|
||||||
|
return { w: 1, h: 1 }; // Mobile: 1x1, compact (round avatar)
|
||||||
}
|
}
|
||||||
return { w: 2, h: 1 }; // Desktop: 2x1 from the start
|
return { w: 1, h: 2 }; // Desktop (all widths): 1x2 vertical, mood sits below
|
||||||
},
|
},
|
||||||
// Column-aware max size: same as defaultSize to prevent further expansion
|
// Column-aware max size: same as defaultSize to prevent expansion
|
||||||
maxAutoSize: (columns) => {
|
maxAutoSize: (columns) => {
|
||||||
if (columns <= 2) {
|
const isMobile = window.innerWidth <= 1000;
|
||||||
return { w: 1, h: 1 }; // Mobile: 1x1, horizontal layout
|
if (isMobile) {
|
||||||
|
return { w: 1, h: 1 }; // Mobile: 1x1, compact
|
||||||
}
|
}
|
||||||
return { w: 2, h: 1 }; // Desktop: 2x1, mood sits in top-right
|
return { w: 1, h: 2 }; // Desktop: 1x2 vertical, mood below at y:2
|
||||||
},
|
},
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|
||||||
@@ -89,15 +92,22 @@ export function registerUserInfoWidget(registry, dependencies) {
|
|||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<div class="rpg-user-info-container" style="${backgroundStyle}">
|
<div class="rpg-user-info-container" style="${backgroundStyle}">
|
||||||
<div class="rpg-user-info-text">
|
${finalConfig.showAvatar ? `<img class="rpg-user-avatar-img" src="${userPortrait}" alt="User Avatar">` : ''}
|
||||||
${finalConfig.showName ? `<div class="rpg-user-name">${userName}</div>` : ''}
|
|
||||||
${finalConfig.showLevel ? `
|
${finalConfig.showName ? `
|
||||||
|
<div class="rpg-user-name-container">
|
||||||
|
<div class="rpg-user-name">${userName}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${finalConfig.showLevel ? `
|
||||||
|
<div class="rpg-user-level-container">
|
||||||
<div class="rpg-user-level">
|
<div class="rpg-user-level">
|
||||||
<span class="rpg-level-label">LVL</span>
|
<span class="rpg-level-label">LVL</span>
|
||||||
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${settings.level}</span>
|
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${settings.level}</span>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
</div>
|
||||||
</div>
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -155,11 +165,15 @@ export function registerUserInfoWidget(registry, dependencies) {
|
|||||||
const infoContainer = container.querySelector('.rpg-user-info-container');
|
const infoContainer = container.querySelector('.rpg-user-info-container');
|
||||||
if (!infoContainer) return;
|
if (!infoContainer) return;
|
||||||
|
|
||||||
// Apply compact mode class at narrow widths for smaller text
|
// Apply layout classes based on widget width
|
||||||
if (newW < 3) {
|
if (newW >= 2) {
|
||||||
infoContainer.classList.add('rpg-user-info-compact');
|
// Wide layout (2x1+): Horizontal split with name left, level right
|
||||||
} else {
|
infoContainer.classList.add('rpg-user-info-wide');
|
||||||
infoContainer.classList.remove('rpg-user-info-compact');
|
infoContainer.classList.remove('rpg-user-info-compact');
|
||||||
|
} else {
|
||||||
|
// Compact layout (1x1): Round avatar with flush text overlays
|
||||||
|
infoContainer.classList.add('rpg-user-info-compact');
|
||||||
|
infoContainer.classList.remove('rpg-user-info-wide');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -33,13 +33,19 @@ export function registerUserStatsWidget(registry, dependencies) {
|
|||||||
description: 'Health, energy, satiety bars',
|
description: 'Health, energy, satiety bars',
|
||||||
category: 'user',
|
category: 'user',
|
||||||
minSize: { w: 1, h: 2 },
|
minSize: { w: 1, h: 2 },
|
||||||
defaultSize: { w: 2, h: 2 },
|
// Column-aware sizing: narrow and tall at 2 cols, wider at 3+ cols
|
||||||
// Column-aware max size: full width in 3-4 col for horizontal spread
|
defaultSize: (columns) => {
|
||||||
|
if (columns <= 2) {
|
||||||
|
return { w: 1, h: 3 }; // Mobile: 1 col wide, 3 rows tall
|
||||||
|
}
|
||||||
|
return { w: 2, h: 3 }; // Desktop: 2 cols wide, 3 rows tall
|
||||||
|
},
|
||||||
|
// Column-aware max size: same as default to prevent expansion
|
||||||
maxAutoSize: (columns) => {
|
maxAutoSize: (columns) => {
|
||||||
if (columns <= 2) {
|
if (columns <= 2) {
|
||||||
return { w: 2, h: 2 }; // Mobile: use full 2-col width
|
return { w: 1, h: 3 }; // Mobile: 1x3
|
||||||
}
|
}
|
||||||
return { w: 3, h: 3 }; // Desktop: span 3 columns horizontally
|
return { w: 2, h: 3 }; // Desktop: 2x3
|
||||||
},
|
},
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ function separateEmojiFromText(str) {
|
|||||||
function stripBrackets(text) {
|
function stripBrackets(text) {
|
||||||
if (!text) return text;
|
if (!text) return text;
|
||||||
|
|
||||||
|
const originalLength = text.length;
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Input length:', originalLength);
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Contains "Skills:":', text.includes('Skills:'));
|
||||||
|
|
||||||
// Remove leading and trailing whitespace first
|
// Remove leading and trailing whitespace first
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
|
|
||||||
@@ -67,6 +71,7 @@ function stripBrackets(text) {
|
|||||||
(text.startsWith('(') && text.endsWith(')'))
|
(text.startsWith('(') && text.endsWith(')'))
|
||||||
) {
|
) {
|
||||||
text = text.substring(1, text.length - 1).trim();
|
text = text.substring(1, text.length - 1).trim();
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Removed wrapping brackets, new length:', text.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove placeholder text patterns like [Location], [Mood Emoji], [Name], etc.
|
// Remove placeholder text patterns like [Location], [Mood Emoji], [Name], etc.
|
||||||
@@ -102,23 +107,103 @@ function stripBrackets(text) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Replace placeholders with empty string, keep real content
|
// Replace placeholders with empty string, keep real content
|
||||||
|
let removedPlaceholders = [];
|
||||||
text = text.replace(placeholderPattern, (match, content) => {
|
text = text.replace(placeholderPattern, (match, content) => {
|
||||||
if (isPlaceholder(match, content)) {
|
if (isPlaceholder(match, content)) {
|
||||||
|
removedPlaceholders.push(match);
|
||||||
return ''; // Remove placeholder
|
return ''; // Remove placeholder
|
||||||
}
|
}
|
||||||
return match; // Keep real bracketed content
|
return match; // Keep real bracketed content
|
||||||
});
|
});
|
||||||
|
if (removedPlaceholders.length > 0) {
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Removed placeholders:', removedPlaceholders.join(', '));
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up any resulting empty labels (e.g., "Status: " with nothing after)
|
// Clean up any resulting empty labels (e.g., "Status: " with nothing after)
|
||||||
text = text.replace(/^([A-Za-z\s]+):\s*$/gm, ''); // Remove lines that are just "Label: " with nothing
|
// BUT: Don't remove structural section headers that have content on following lines
|
||||||
|
const beforeCleanup = text.length;
|
||||||
|
|
||||||
|
// Known section headers that should NEVER be removed (structural markers)
|
||||||
|
const structuralHeaders = ['Skills', 'Status', 'Inventory', 'On Person', 'Stored', 'Assets', 'Main Quest', 'Main Quests', 'Optional Quest', 'Optional Quests'];
|
||||||
|
|
||||||
|
// Split into lines to intelligently remove only truly empty labels
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const filteredLines = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
// Check if this is a label line (ends with colon, no other content)
|
||||||
|
const labelMatch = trimmedLine.match(/^([A-Za-z\s]+):\s*$/);
|
||||||
|
|
||||||
|
if (labelMatch) {
|
||||||
|
const labelName = labelMatch[1];
|
||||||
|
|
||||||
|
// Never remove structural section headers
|
||||||
|
if (structuralHeaders.includes(labelName)) {
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Keeping structural header:', trimmedLine);
|
||||||
|
filteredLines.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's ANY content in the next few lines (look ahead up to 3 lines)
|
||||||
|
let hasContentBelow = false;
|
||||||
|
for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
|
||||||
|
const futureLine = lines[j].trim();
|
||||||
|
if (futureLine === '') continue; // Skip empty lines
|
||||||
|
|
||||||
|
// If we find a line with content (not just another label), this label has content below
|
||||||
|
if (futureLine && !/^([A-Za-z\s]+):\s*$/.test(futureLine)) {
|
||||||
|
hasContentBelow = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasContentBelow) {
|
||||||
|
// This label has content below (even if through other labels), keep it
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Keeping section header:', trimmedLine);
|
||||||
|
filteredLines.push(line);
|
||||||
|
} else {
|
||||||
|
// This is a truly empty label with no content anywhere below, remove it
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Removing empty label:', trimmedLine);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a label line, keep it
|
||||||
|
filteredLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text = filteredLines.join('\n');
|
||||||
|
|
||||||
|
if (text.length !== beforeCleanup) {
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Removed empty labels, chars removed:', beforeCleanup - text.length);
|
||||||
|
}
|
||||||
|
|
||||||
text = text.replace(/^([A-Za-z\s]+):\s*,/gm, '$1:'); // Fix "Label: ," patterns
|
text = text.replace(/^([A-Za-z\s]+):\s*,/gm, '$1:'); // Fix "Label: ," patterns
|
||||||
text = text.replace(/:\s*\|/g, ':'); // Fix ": |" patterns
|
text = text.replace(/:\s*\|/g, ':'); // Fix ": |" patterns
|
||||||
text = text.replace(/\|\s*\|/g, '|'); // Fix "| |" patterns (double pipes from removed content)
|
text = text.replace(/\|\s*\|/g, '|'); // Fix "| |" patterns (double pipes from removed content)
|
||||||
text = text.replace(/\|\s*$/gm, ''); // Remove trailing pipes at end of lines
|
text = text.replace(/\|\s*$/gm, ''); // Remove trailing pipes at end of lines
|
||||||
|
|
||||||
// Clean up multiple spaces and empty lines
|
// Clean up multiple spaces and empty lines
|
||||||
|
const beforeSpaceCleanup = text.length;
|
||||||
text = text.replace(/\s{2,}/g, ' '); // Multiple spaces to single space
|
text = text.replace(/\s{2,}/g, ' '); // Multiple spaces to single space
|
||||||
text = text.replace(/^\s*\n/gm, ''); // Remove empty lines
|
text = text.replace(/^\s*\n/gm, ''); // Remove empty lines
|
||||||
|
if (text.length !== beforeSpaceCleanup) {
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Cleaned up spaces/newlines, chars removed:', beforeSpaceCleanup - text.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalLength = text.trim().length;
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Output length:', finalLength);
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Total chars removed:', originalLength - finalLength);
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Contains "Skills:" after processing:', text.includes('Skills:'));
|
||||||
|
|
||||||
|
if (text.includes('Skills:')) {
|
||||||
|
const skillsIndex = text.indexOf('Skills:');
|
||||||
|
debugLog('[RPG Parser] stripBrackets: Text around Skills (index ' + skillsIndex + '):', text.substring(skillsIndex, skillsIndex + 200));
|
||||||
|
} else if (originalLength !== finalLength) {
|
||||||
|
debugLog('[RPG Parser] stripBrackets: WARNING - Skills section was removed! Last 200 chars:', text.substring(text.length - 200));
|
||||||
|
}
|
||||||
|
|
||||||
return text.trim();
|
return text.trim();
|
||||||
}
|
}
|
||||||
@@ -133,6 +218,164 @@ function debugLog(message, data = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract structured skills data from stats text
|
||||||
|
* Parses format:
|
||||||
|
* Skills:
|
||||||
|
* CategoryName:
|
||||||
|
* - SkillName (Lv X)
|
||||||
|
* - SkillName (Lv X)
|
||||||
|
* Uncategorized:
|
||||||
|
* - SkillName (Lv X)
|
||||||
|
*
|
||||||
|
* @param {string} statsText - Stats section text containing skills
|
||||||
|
* @returns {Object|null} Structured skills data or null if not found
|
||||||
|
*/
|
||||||
|
function extractSkills(statsText) {
|
||||||
|
if (!statsText) {
|
||||||
|
debugLog('[RPG Parser] extractSkills: No stats text provided');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('[RPG Parser] extractSkills: Searching for Skills section in text length:', statsText.length);
|
||||||
|
debugLog('[RPG Parser] extractSkills: Text contains "Skills:":', statsText.includes('Skills:'));
|
||||||
|
|
||||||
|
// Find the Skills section
|
||||||
|
const skillsMatch = statsText.match(/Skills:([\s\S]*?)(?=\n\n|On Person:|Stored|Assets:|Main Quest|Optional Quest|$)/i);
|
||||||
|
if (!skillsMatch) {
|
||||||
|
debugLog('[RPG Parser] extractSkills: Main regex did not match');
|
||||||
|
debugLog('[RPG Parser] extractSkills: Checking if "On Person:" exists:', statsText.includes('On Person:'));
|
||||||
|
debugLog('[RPG Parser] extractSkills: Text around Skills:', statsText.substring(statsText.indexOf('Skills:'), statsText.indexOf('Skills:') + 200));
|
||||||
|
|
||||||
|
// Fallback: try simple format "Skills: skill1, skill2"
|
||||||
|
const simpleMatch = statsText.match(/Skills:\s*(.+)/i);
|
||||||
|
if (simpleMatch) {
|
||||||
|
const skillsText = simpleMatch[1].trim();
|
||||||
|
debugLog('[RPG Parser] extractSkills: Simple format matched:', skillsText);
|
||||||
|
if (skillsText && skillsText !== 'None') {
|
||||||
|
// Return as string for backward compatibility
|
||||||
|
return skillsText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugLog('[RPG Parser] extractSkills: No Skills section found');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('[RPG Parser] extractSkills: Main regex matched, captured length:', skillsMatch[1].length);
|
||||||
|
|
||||||
|
const skillsSection = skillsMatch[1];
|
||||||
|
const skillsData = {
|
||||||
|
version: 1,
|
||||||
|
categories: {},
|
||||||
|
uncategorized: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split into lines and process
|
||||||
|
const lines = skillsSection.split('\n').map(line => line.trim()).filter(line => line);
|
||||||
|
|
||||||
|
debugLog('[RPG Parser] Skills section lines:', lines);
|
||||||
|
|
||||||
|
let currentCategory = null;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Check if this is a category header (ends with colon, no dash)
|
||||||
|
if (line.endsWith(':') && !line.startsWith('-')) {
|
||||||
|
currentCategory = line.slice(0, -1).trim();
|
||||||
|
debugLog(`[RPG Parser] Found category header: "${currentCategory}"`);
|
||||||
|
if (currentCategory !== 'Uncategorized' && !skillsData.categories[currentCategory]) {
|
||||||
|
skillsData.categories[currentCategory] = [];
|
||||||
|
debugLog(`[RPG Parser] Created category array for: "${currentCategory}"`);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a skill line (starts with -, has level info)
|
||||||
|
// Try numeric format first: "- Skill Name (Lv 5)"
|
||||||
|
let skillMatch = line.match(/^-\s*(.+?)\s*\(Lv\s*(\d+)\)/i);
|
||||||
|
if (skillMatch) {
|
||||||
|
const skillName = skillMatch[1].trim();
|
||||||
|
const level = parseInt(skillMatch[2], 10) || 1;
|
||||||
|
|
||||||
|
const skill = {
|
||||||
|
name: skillName,
|
||||||
|
level: level,
|
||||||
|
xp: 0,
|
||||||
|
maxXP: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentCategory === 'Uncategorized' || currentCategory === null) {
|
||||||
|
debugLog(`[RPG Parser] Adding "${skillName}" to uncategorized (currentCategory="${currentCategory}")`);
|
||||||
|
skillsData.uncategorized.push(skill);
|
||||||
|
} else if (currentCategory && skillsData.categories[currentCategory]) {
|
||||||
|
debugLog(`[RPG Parser] Adding "${skillName}" to category "${currentCategory}"`);
|
||||||
|
skillsData.categories[currentCategory].push(skill);
|
||||||
|
} else {
|
||||||
|
debugLog(`[RPG Parser] ERROR: Could not add "${skillName}" - currentCategory="${currentCategory}", categoryExists=${!!skillsData.categories[currentCategory]}`);
|
||||||
|
// Fallback to uncategorized if category doesn't exist
|
||||||
|
skillsData.uncategorized.push(skill);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: Try text-based proficiency format: "- Skill Name (Proficient)"
|
||||||
|
const textMatch = line.match(/^-\s*(.+?)\s*\((.+?)\)/i);
|
||||||
|
if (textMatch) {
|
||||||
|
const skillName = textMatch[1].trim();
|
||||||
|
const proficiencyText = textMatch[2].trim().toLowerCase();
|
||||||
|
|
||||||
|
// Map text proficiency to numeric level
|
||||||
|
const proficiencyMap = {
|
||||||
|
'initiated': 1,
|
||||||
|
'novice': 1,
|
||||||
|
'basic': 2,
|
||||||
|
'beginner': 2,
|
||||||
|
'intermediate': 4,
|
||||||
|
'proficient': 5,
|
||||||
|
'competent': 6,
|
||||||
|
'advanced': 7,
|
||||||
|
'expert': 8,
|
||||||
|
'mastered': 9,
|
||||||
|
'master': 9,
|
||||||
|
'grandmaster': 10,
|
||||||
|
'legendary': 10
|
||||||
|
};
|
||||||
|
|
||||||
|
const level = proficiencyMap[proficiencyText] || 5; // Default to 5 if unknown
|
||||||
|
|
||||||
|
const skill = {
|
||||||
|
name: skillName,
|
||||||
|
level: level,
|
||||||
|
xp: 0,
|
||||||
|
maxXP: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
if (currentCategory === 'Uncategorized' || currentCategory === null) {
|
||||||
|
debugLog(`[RPG Parser] Adding "${skillName}" to uncategorized (currentCategory="${currentCategory}")`);
|
||||||
|
skillsData.uncategorized.push(skill);
|
||||||
|
} else if (currentCategory && skillsData.categories[currentCategory]) {
|
||||||
|
debugLog(`[RPG Parser] Adding "${skillName}" to category "${currentCategory}"`);
|
||||||
|
skillsData.categories[currentCategory].push(skill);
|
||||||
|
} else {
|
||||||
|
debugLog(`[RPG Parser] ERROR: Could not add "${skillName}" - currentCategory="${currentCategory}", categoryExists=${!!skillsData.categories[currentCategory]}`);
|
||||||
|
// Fallback to uncategorized if category doesn't exist
|
||||||
|
skillsData.uncategorized.push(skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return null if no skills were found
|
||||||
|
if (Object.keys(skillsData.categories).length === 0 && skillsData.uncategorized.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLog('[RPG Parser] Final skills data:', {
|
||||||
|
categories: Object.keys(skillsData.categories),
|
||||||
|
categoryCounts: Object.entries(skillsData.categories).map(([cat, skills]) => `${cat}: ${skills.length}`),
|
||||||
|
uncategorizedCount: skillsData.uncategorized.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return skillsData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the model response to extract the different data sections.
|
* Parses the model response to extract the different data sections.
|
||||||
* Extracts tracker data from markdown code blocks in the AI response.
|
* Extracts tracker data from markdown code blocks in the AI response.
|
||||||
@@ -170,7 +413,13 @@ export function parseResponse(responseText) {
|
|||||||
const content = match[1].trim();
|
const content = match[1].trim();
|
||||||
|
|
||||||
debugLog(`[RPG Parser] --- Code Block ${i + 1} ---`);
|
debugLog(`[RPG Parser] --- Code Block ${i + 1} ---`);
|
||||||
|
debugLog('[RPG Parser] Content length:', content.length);
|
||||||
debugLog('[RPG Parser] First 300 chars:', content.substring(0, 300));
|
debugLog('[RPG Parser] First 300 chars:', content.substring(0, 300));
|
||||||
|
debugLog('[RPG Parser] Contains "Skills:":', content.includes('Skills:'));
|
||||||
|
if (content.includes('Skills:')) {
|
||||||
|
const skillsIndex = content.indexOf('Skills:');
|
||||||
|
debugLog('[RPG Parser] Text around Skills (index ' + skillsIndex + '):', content.substring(skillsIndex, skillsIndex + 200));
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a combined code block with multiple sections
|
// Check if this is a combined code block with multiple sections
|
||||||
const hasMultipleSections = (
|
const hasMultipleSections = (
|
||||||
@@ -351,10 +600,12 @@ export function parseUserStats(statsText) {
|
|||||||
// Parse skills section if enabled
|
// Parse skills section if enabled
|
||||||
const skillsConfig = trackerConfig?.userStats?.skillsSection;
|
const skillsConfig = trackerConfig?.userStats?.skillsSection;
|
||||||
if (skillsConfig?.enabled) {
|
if (skillsConfig?.enabled) {
|
||||||
const skillsMatch = statsText.match(/Skills:\s*(.+)/i);
|
const skillsData = extractSkills(statsText);
|
||||||
if (skillsMatch) {
|
if (skillsData) {
|
||||||
extensionSettings.userStats.skills = skillsMatch[1].trim();
|
extensionSettings.userStats.skills = skillsData;
|
||||||
debugLog('[RPG Parser] Skills extracted:', skillsMatch[1].trim());
|
debugLog('[RPG Parser] Skills extracted:', skillsData);
|
||||||
|
} else {
|
||||||
|
debugLog('[RPG Parser] Skills extraction failed or none found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,53 @@ import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../co
|
|||||||
// Type imports
|
// Type imports
|
||||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a formatted skills summary for AI context injection.
|
||||||
|
* Converts structured skills data to multi-line plaintext format organized by category.
|
||||||
|
*
|
||||||
|
* @param {Object|string} skills - Current skills (structured or legacy string)
|
||||||
|
* @returns {string} Formatted skills summary for prompt injection
|
||||||
|
* @example
|
||||||
|
* // Structured input: { version: 1, categories: { Combat: [{name: 'Swordsmanship', level: 5}] }, uncategorized: [] }
|
||||||
|
* // Returns: "Skills:\nCombat:\n- Swordsmanship (Lv 5)"
|
||||||
|
*/
|
||||||
|
export function buildSkillsSummary(skills) {
|
||||||
|
// Handle legacy string format
|
||||||
|
if (typeof skills === 'string') {
|
||||||
|
return `Skills: ${skills}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle structured format
|
||||||
|
if (skills && typeof skills === 'object' && skills.version) {
|
||||||
|
let summary = 'Skills:';
|
||||||
|
const categories = skills.categories || {};
|
||||||
|
const uncategorized = skills.uncategorized || [];
|
||||||
|
|
||||||
|
// Add categorized skills
|
||||||
|
for (const [categoryName, skillsList] of Object.entries(categories)) {
|
||||||
|
if (skillsList && skillsList.length > 0) {
|
||||||
|
summary += `\n${categoryName}:`;
|
||||||
|
for (const skill of skillsList) {
|
||||||
|
summary += `\n- ${skill.name} (Lv ${skill.level})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add uncategorized skills
|
||||||
|
if (uncategorized.length > 0) {
|
||||||
|
summary += '\nUncategorized:';
|
||||||
|
for (const skill of uncategorized) {
|
||||||
|
summary += `\n- ${skill.name} (Lv ${skill.level})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty or invalid
|
||||||
|
return 'Skills: None';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a formatted inventory summary for AI context injection.
|
* Builds a formatted inventory summary for AI context injection.
|
||||||
* Converts v2 inventory structure to multi-line plaintext format.
|
* Converts v2 inventory structure to multi-line plaintext format.
|
||||||
@@ -166,9 +213,13 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
|
|
||||||
// Add skills section if enabled
|
// Add skills section if enabled
|
||||||
if (userStatsConfig?.skillsSection?.enabled) {
|
if (userStatsConfig?.skillsSection?.enabled) {
|
||||||
const skillFields = userStatsConfig.skillsSection.customFields || [];
|
instructions += `Skills:\n`;
|
||||||
const skillFieldsText = skillFields.map(f => `[${f}]`).join(', ');
|
instructions += `[Category Name]:\n`;
|
||||||
instructions += `Skills: [${skillFieldsText || 'Skill1, Skill2, etc.'}]\n`;
|
instructions += `- [Skill Name] (Lv [1-100])\n`;
|
||||||
|
instructions += `- [Another Skill] (Lv [1-100])\n`;
|
||||||
|
instructions += `Uncategorized:\n`;
|
||||||
|
instructions += `- [Uncategorized Skill] (Lv [1-100])\n`;
|
||||||
|
instructions += `(Organize skills by logical categories like Combat, Magic, Social, Crafting, etc. IMPORTANT: Use numeric levels only - write "Lv 5" not "Proficient", "Lv 7" not "Advanced". Use integers 1-100 where 1=novice, 5=intermediate, 10=expert. Skills without a clear category go in Uncategorized.)\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add inventory format based on feature flag
|
// Add inventory format based on feature flag
|
||||||
|
|||||||
+3
-8
@@ -59,15 +59,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Manual Update Button -->
|
<!-- Action Buttons Row -->
|
||||||
<button id="rpg-manual-update" class="rpg-btn-primary rpg-manual-update-btn">
|
|
||||||
<i class="fa-solid fa-sync"></i> Refresh RPG Info
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Settings and Edit Trackers Buttons Row -->
|
|
||||||
<div class="rpg-settings-buttons-row">
|
<div class="rpg-settings-buttons-row">
|
||||||
<button id="rpg-open-tracker-editor" class="rpg-btn-settings rpg-btn-half">
|
<button id="rpg-manual-update" class="rpg-btn-primary rpg-manual-update-btn rpg-btn-half">
|
||||||
<i class="fa-solid fa-sliders"></i> Edit Trackers
|
<i class="fa-solid fa-sync"></i> Refresh RPG Info
|
||||||
</button>
|
</button>
|
||||||
<button id="rpg-open-settings" class="rpg-btn-settings rpg-btn-half">
|
<button id="rpg-open-settings" class="rpg-btn-settings rpg-btn-half">
|
||||||
<i class="fa-solid fa-gear"></i> Settings
|
<i class="fa-solid fa-gear"></i> Settings
|
||||||
|
|||||||
Reference in New Issue
Block a user