diff --git a/WorldEditor/WorldEditor.css b/WorldEditor/WorldEditor.css new file mode 100644 index 0000000..abfad63 --- /dev/null +++ b/WorldEditor/WorldEditor.css @@ -0,0 +1,344 @@ +/* 世界书编辑器样式 */ +#world-editor-container .world-editor { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: #ffffff; + overflow: hidden; + display: flex; + flex-direction: column; +} + +#world-editor-container .world-editor-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background-color: #2d2d2d; + border-bottom: 1px solid #555; + flex-shrink: 0; +} + +#world-editor-container .world-editor-header h1 { + margin: 0; + color: #68b7ff; + font-size: 20px; +} + +#world-editor-container .world-editor-header-controls { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +#world-editor-container .world-editor-btn { + padding: 6px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: background-color 0.3s; +} + +#world-editor-container .world-editor-btn-primary { + background-color: #4a90e2; + color: white; +} + +#world-editor-container .world-editor-btn-primary:hover { + background-color: #357abd; +} + +#world-editor-container .world-editor-btn-success { + background-color: #4CAF50; + color: white; +} + +#world-editor-container .world-editor-btn-success:hover { + background-color: #45a049; +} + +#world-editor-container .world-editor-btn-danger { + background-color: #f44336; + color: white; +} + +#world-editor-container .world-editor-btn-danger:hover { + background-color: #da190b; +} + +#world-editor-container .world-editor-btn-warning { + background-color: #ff9800; + color: white; +} + +#world-editor-container .world-editor-btn-warning:hover { + background-color: #e68900; +} + +#world-editor-container .world-editor-content { + flex: 1; + padding: 20px; + overflow: auto; +} + +#world-editor-container .world-editor-selector { + margin-bottom: 15px; + padding: 15px; + background-color: #2d2d2d; + border-radius: 8px; +} + +#world-editor-container .world-editor-selector select { + width: 100%; + padding: 8px; + background-color: #404040; + color: white; + border: 1px solid #555; + border-radius: 4px; +} + +#world-editor-container .world-editor-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + padding: 10px 15px; + background-color: #2d2d2d; + border-radius: 8px; +} + +#world-editor-container .world-editor-toolbar-left { + display: flex; + gap: 10px; + align-items: center; +} + +#world-editor-container .world-editor-toolbar-right { + display: flex; + gap: 10px; + align-items: center; +} + +#world-editor-container .world-editor-search-box { + padding: 6px 10px; + background-color: #404040; + color: white; + border: 1px solid #555; + border-radius: 4px; + min-width: 200px; +} + +#world-editor-container .world-editor-entries-container { + background-color: #2d2d2d; + border-radius: 8px; + overflow-y: auto; + max-height: calc(100vh - 300px); /* Adjust as needed */ + position: relative; +} + +#world-editor-container .world-editor-entries-header { + display: grid; + grid-template-columns: 40px 50px 50px 120px 1fr 100px 80px 80px; + background-color: #404040; + padding: 10px; + border-bottom: 1px solid #555; + font-weight: bold; + font-size: 12px; + position: sticky; + top: 0; + z-index: 1; +} + +#world-editor-container .world-editor-entries-header > div[data-sort] { + cursor: pointer; + user-select: none; +} + +#world-editor-container .world-editor-entries-header > div[data-sort]:hover { + color: #68b7ff; +} + +#world-editor-container .world-editor-entry-row { + display: grid; + grid-template-columns: 40px 50px 50px 120px 1fr 100px 80px 80px; + padding: 8px 10px; + border-bottom: 1px solid #333; + align-items: center; + transition: background-color 0.2s; + min-height: 40px; + cursor: pointer; +} + + +#world-editor-container .world-editor-entry-row:hover { + background-color: #3a3a3a; +} + +#world-editor-container .world-editor-entry-row.selected { + background-color: rgba(74, 74, 74, 0.5); +} + +#world-editor-container .world-editor-entry-checkbox { + width: 16px; + height: 16px; + justify-self: center; +} + +#world-editor-container .world-editor-entry-status { + text-align: center; + font-size: 18px; + cursor: pointer; +} + +#world-editor-container .world-editor-status-enabled { + color: #4CAF50; +} + +#world-editor-container .world-editor-status-disabled { + color: #f44336; +} + +#world-editor-container .fa-toggle-on { + color: #68b7ff; +} + +#world-editor-container .fa-toggle-off { + color: #777; +} + +#world-editor-container .world-editor-entry-activation { + text-align: center; + font-size: 16px; + cursor: pointer; +} + +#world-editor-container .world-editor-activation-constant { + color: #2196F3; +} + +#world-editor-container .world-editor-activation-selective { + color: #4CAF50; +} + +#world-editor-container .world-editor-entry-keys { + font-size: 11px; + color: #aaa; + max-width: 140px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#world-editor-container .world-editor-entry-content { + font-size: 11px; + color: #ccc; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#world-editor-container .world-editor-entry-position { + font-size: 10px; + color: #999; + text-align: center; +} + +#world-editor-container .world-editor-entry-depth { + font-size: 12px; + color: #fff; + text-align: center; + background-color: #555; + border-radius: 3px; + padding: 2px 6px; + cursor: pointer; +} + +#world-editor-container .world-editor-entry-order { + font-size: 12px; + color: #fff; + text-align: center; +} + +/* 行内编辑样式 */ +#world-editor-container .inline-edit { + background-color: transparent; + border: 1px solid transparent; + color: #ccc; + font-family: inherit; + font-size: 11px; + padding: 2px 4px; + width: 100%; + box-sizing: border-box; + border-radius: 3px; + transition: border-color 0.2s, background-color 0.2s; +} + +#world-editor-container .inline-edit:hover { + border-color: #555; +} + +#world-editor-container .inline-edit:focus { + background-color: #404040; + border-color: #68b7ff; + outline: none; +} + +#world-editor-container .inline-edit[disabled] { + background-color: rgba(0,0,0,0.2); + color: #777; + cursor: not-allowed; +} + +#world-editor-container .inline-toggle { + cursor: pointer; + text-align: center; +} + +#world-editor-container .world-editor-batch-actions { + display: none; + padding: 15px; + background-color: #4a4a4a; + border-bottom: 1px solid #555; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +#world-editor-container .world-editor-batch-actions.active { + display: flex; +} + +#world-editor-container .world-editor-selected-count { + color: #68b7ff; + font-weight: bold; +} + + +#world-editor-container .world-editor-loading { + text-align: center; + padding: 40px; + color: #999; +} + +#world-editor-container .world-editor-empty-state { + text-align: center; + padding: 40px; + color: #999; +} + +#world-editor-container .world-editor-error { + background-color: #d32f2f; + color: white; + padding: 10px; + border-radius: 4px; + margin-bottom: 15px; +} + +#world-editor-container .world-editor-success { + background-color: #388e3c; + color: white; + padding: 10px; + border-radius: 4px; + margin-bottom: 15px; +} diff --git a/WorldEditor/WorldEditor.js b/WorldEditor/WorldEditor.js new file mode 100644 index 0000000..ec82c2f --- /dev/null +++ b/WorldEditor/WorldEditor.js @@ -0,0 +1,525 @@ +/** + * 世界书编辑器 - 最终稳定版 + */ +import { world_names, loadWorldInfo, saveWorldInfo } from "/scripts/world-info.js"; +import { eventSource, event_types } from '/script.js'; +import { showHtmlModal } from '/scripts/extensions/third-party/ST-Amily2-Chat-Optimisation/ui/page-window.js'; +const { SillyTavern, TavernHelper } = window; + +class WorldEditor { + constructor() { + this.currentWorldBook = null; + this.entries = []; + this.selectedEntries = new Set(); + this.filteredEntries = []; + this.isLoading = false; + this.currentEditingEntry = null; + this.sortState = { key: 'order', asc: true }; + this.init(); + } + + init() { + if (!this.initializeComponents()) { + console.error('[世界书编辑器] 组件初始化失败,5秒后重试...'); + setTimeout(() => this.init(), 5000); + return; + } + this.bindEvents(); + this.loadAvailableWorldBooks(); + this.bindExternalEvents(); // 绑定外部事件监听 + } + + initializeComponents() { + const ids = [ + 'world-editor-world-select', 'world-editor-refresh-btn', 'world-editor-create-entry-btn', + 'world-editor-search-box', 'world-editor-search-btn', 'world-editor-entry-count', + 'world-editor-select-all', 'world-editor-selected-count', 'world-editor-batch-actions', + 'world-editor-entries-container', + 'world-editor-enable-selected-btn', 'world-editor-disable-selected-btn', + 'world-editor-set-blue-btn', 'world-editor-set-green-btn', 'world-editor-delete-selected-btn', + 'world-editor-set-disable-recursion-btn', 'world-editor-set-prevent-recursion-btn' + ]; + this.elements = {}; + let missing = false; + for (const id of ids) { + const camelCaseId = id.replace(/-(\w)/g, (_, c) => c.toUpperCase()); + this.elements[camelCaseId] = document.getElementById(id); + if (!this.elements[camelCaseId] && id.endsWith('container')) { // Only container is critical + console.error(`[世界书编辑器] 关键元素缺失: ${id}`); + missing = true; + } + } + return !missing; + } + + bindEvents() { + this.elements.worldEditorWorldSelect.addEventListener('change', (e) => this.loadWorldBookEntries(e.target.value)); + this.elements.worldEditorRefreshBtn.addEventListener('click', () => this.loadAvailableWorldBooks()); + document.querySelector('#world-editor-container .world-editor-entries-header').addEventListener('click', (e) => { + if (e.target.dataset.sort) { + this.sortEntries(e.target.dataset.sort); + } + }); + this.elements.worldEditorCreateEntryBtn.addEventListener('click', () => this.openCreateModal()); + this.elements.worldEditorSearchBox.addEventListener('input', () => this.filterEntries()); + this.elements.worldEditorSearchBtn.addEventListener('click', () => this.filterEntries()); + this.elements.worldEditorSelectAll.addEventListener('change', (e) => this.toggleSelectAll(e.target.checked)); + this.elements.worldEditorEnableSelectedBtn.addEventListener('click', () => this.batchUpdateEntries({ enabled: true })); + this.elements.worldEditorDisableSelectedBtn.addEventListener('click', () => this.batchUpdateEntries({ enabled: false })); + this.elements.worldEditorSetBlueBtn.addEventListener('click', () => this.batchUpdateEntries({ type: 'constant' })); + this.elements.worldEditorSetGreenBtn.addEventListener('click', () => this.batchUpdateEntries({ type: 'selective' })); + this.elements.worldEditorDeleteSelectedBtn.addEventListener('click', () => this.batchDeleteEntries()); + this.elements.worldEditorSetDisableRecursionBtn.addEventListener('click', () => this.toggleBatchRecursion('exclude_recursion', '不可递归')); + this.elements.worldEditorSetPreventRecursionBtn.addEventListener('click', () => this.toggleBatchRecursion('prevent_recursion', '防止递归')); + } + + async loadAvailableWorldBooks() { + this.setLoading(true); + try { + const books = await this.getAllWorldBooks(); + const select = this.elements.worldEditorWorldSelect; + select.innerHTML = ''; + books.forEach(book => { + const option = document.createElement('option'); + option.value = book.name; + option.textContent = book.name; + select.appendChild(option); + }); + await this.selectCurrentCharacterWorldBook(); + } catch (error) { + this.showError('加载世界书列表失败: ' + error.message); + } finally { + this.setLoading(false); + } + } + + async getAllWorldBooks() { + if (TavernHelper?.getLorebooks) { + const books = await TavernHelper.getLorebooks(); + if (Array.isArray(books) && books.length > 0) return books.map(name => ({ name })); + } + return (world_names || []).map(name => ({ name })); + } + + async selectCurrentCharacterWorldBook() { + if (TavernHelper?.getCurrentCharPrimaryLorebook) { + const primaryBook = await TavernHelper.getCurrentCharPrimaryLorebook(); + if (primaryBook) { + this.elements.worldEditorWorldSelect.value = primaryBook; + await this.loadWorldBookEntries(primaryBook); + } + } + } + + async loadWorldBookEntries(worldBookName) { + if (!worldBookName) { this.entries = []; this.renderEntries(); return; } + this.setLoading(true); + this.currentWorldBook = worldBookName; + try { + let rawEntries = await TavernHelper?.getLorebookEntries?.(worldBookName); + if (!rawEntries || rawEntries.length === 0) { + const bookData = await loadWorldInfo(worldBookName); + if (bookData?.entries) { + rawEntries = Object.entries(bookData.entries).map(([uid, entry]) => ({ + uid: parseInt(uid), enabled: !entry.disable, type: entry.constant ? 'constant' : 'selective', + keys: entry.key || [], content: entry.content || '', position: this.convertPositionFromNative(entry.position), + depth: entry.depth, order: entry.order, comment: entry.comment || '' + })); + } + } + this.entries = (rawEntries || []).map(e => ({ + uid: e.uid, enabled: e.enabled, type: e.type || (e.constant ? 'constant' : 'selective'), + keys: e.keys || [], content: e.content || '', position: e.position || 'before_character_definition', + depth: (e.position?.startsWith('at_depth')) ? e.depth : null, order: e.order || 100, comment: e.comment || '', + exclude_recursion: e.exclude_recursion, prevent_recursion: e.prevent_recursion + })); + this.filteredEntries = [...this.entries]; + this.renderEntries(); + this.updateEntryCount(); + } catch (error) { + this.showError(`加载条目失败: ${error.message}`); + this.entries = []; this.renderEntries(); + } finally { + this.setLoading(false); + } + } + + convertPositionFromNative(pos) { + const map = { 0: 'before_character_definition', 1: 'after_character_definition', 2: 'before_author_note', 3: 'after_author_note', 4: 'at_depth' }; + return map[pos] || 'at_depth'; + } + + renderEntries() { + const container = this.elements.worldEditorEntriesContainer; + const header = container.querySelector('.world-editor-entries-header'); + + // Clear only the entry rows, not the header + while (container.firstChild && container.firstChild !== header) { + container.removeChild(container.firstChild); + } + while (header && header.nextSibling) { + container.removeChild(header.nextSibling); + } + + this.sortFilteredEntries(); + + if (this.filteredEntries.length === 0) { + const emptyState = document.createElement('div'); + emptyState.className = 'world-editor-empty-state'; + emptyState.innerHTML = '
没有条目
'; + container.appendChild(emptyState); + return; + } + + const fragment = document.createDocumentFragment(); + this.filteredEntries.forEach(e => { + const row = document.createElement('div'); + row.innerHTML = this.renderEntryRow(e).trim(); + fragment.appendChild(row.firstChild); + }); + container.appendChild(fragment); + this.bindEntryEvents(); + } + + renderEntryRow(entry) { + const positionOptions = { + 'before_character_definition': '角色前', 'after_character_definition': '角色后', + 'before_author_note': '注释前', 'after_author_note': '注释后', + 'at_depth': '@D深度', 'at_depth_as_system': '@D深度' + }; + const positionSelect = ``; + + return ` +