mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 05:25:51 +00:00
Initial commit with CC BY-NC-ND 4.0 license
This commit is contained in:
626
WorldEditor/WorldEditor.css
Normal file
626
WorldEditor/WorldEditor.css
Normal file
@@ -0,0 +1,626 @@
|
||||
/* 世界书编辑器样式 */
|
||||
#world-editor-container {
|
||||
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;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#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-btn-info {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-btn-info:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
/* 新增:世界书列表样式 */
|
||||
#world-book-list-container {
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 350px); /* 根据需要调整 */
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.world-book-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.world-book-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.world-book-row:hover {
|
||||
background-color: #3f3f3f;
|
||||
}
|
||||
|
||||
.world-book-row.selected {
|
||||
background-color: #4a4a4a;
|
||||
border-left: 3px solid #68b7ff;
|
||||
}
|
||||
|
||||
.world-book-checkbox {
|
||||
margin-right: 15px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.world-book-name {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.world-book-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
opacity: 0; /* 默认隐藏 */
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.world-book-row:hover .world-book-actions {
|
||||
opacity: 1; /* 悬停时显示 */
|
||||
}
|
||||
|
||||
.world-editor-btn.small-btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
color: white;
|
||||
background-color: #4a90e2;
|
||||
}
|
||||
|
||||
.world-editor-btn.small-btn:hover {
|
||||
background-color: #357abd;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-selector h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-selector {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-selector button {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
background-color: #4a90e2;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-selector button:hover {
|
||||
background-color: #357abd;
|
||||
}
|
||||
|
||||
/* 确保返回列表按钮有颜色 */
|
||||
#world-editor-back-to-list-btn {
|
||||
color: white !important;
|
||||
background-color: #4a90e2 !important;
|
||||
}
|
||||
|
||||
#world-editor-back-to-list-btn:hover {
|
||||
background-color: #357abd !important;
|
||||
}
|
||||
|
||||
/* ====== 布局修正 v2:针对 fieldset ====== */
|
||||
|
||||
/* 1. 重置 fieldset 的默认样式,使其表现为标准的 flex 容器 */
|
||||
.settings-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none; /* 移除 fieldset 默认边框 */
|
||||
padding: 0; /* 移除 fieldset 默认内边距 */
|
||||
margin: 0; /* 移除 fieldset 默认外边距 */
|
||||
min-inline-size: auto; /* 覆盖默认行为 */
|
||||
flex: 1; /* 让 fieldset 自身也能在父容器中伸展 */
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 2. 让主容器在 fieldset 内撑满 */
|
||||
#world-editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 3. 让视图(世界书列表 / 条目编辑器)作为flex子项,能够自动填充剩余空间 */
|
||||
#world-book-selection-view,
|
||||
#world-editor-entry-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 4. 条目视图内的包裹容器也需要是flex布局 */
|
||||
.world-editor-entries-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 5. 移除列表的固定高度限制,让它们在flex容器内自由伸展 */
|
||||
#world-book-list-container,
|
||||
#world-editor-container .world-editor-entries-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: none; /* 覆盖之前写死的max-height */
|
||||
}
|
||||
|
||||
/* 响应式设计:移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
#world-editor-container .world-editor-header-controls,
|
||||
#world-editor-container .world-editor-toolbar-left,
|
||||
#world-editor-container .world-editor-toolbar-right {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-search-box {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-entries-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #333;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-entries-header > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-entries-header > div:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-entries-header > div:first-child::after {
|
||||
content: "全选";
|
||||
margin-left: 10px;
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-entry-row {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr; /* 复选框和内容区 */
|
||||
gap: 5px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #444;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-entry-row > div {
|
||||
text-align: left;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* Add labels for mobile view */
|
||||
#world-editor-container .world-editor-entry-row > div::before {
|
||||
content: attr(data-label) ": ";
|
||||
font-weight: bold;
|
||||
color: #888;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Hide label for checkbox */
|
||||
#world-editor-container .world-editor-entry-row > div:nth-child(1)::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Checkbox positioning - Target the WRAPPER div */
|
||||
#world-editor-container .world-editor-entry-checkbox {
|
||||
grid-row: 1 / span 8;
|
||||
align-self: start;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Stack other elements in the second column */
|
||||
#world-editor-container .world-editor-entry-status,
|
||||
#world-editor-container .world-editor-entry-activation,
|
||||
#world-editor-container .world-editor-entry-keys,
|
||||
#world-editor-container .world-editor-entry-content,
|
||||
#world-editor-container .world-editor-entry-position,
|
||||
#world-editor-container .world-editor-entry-depth,
|
||||
#world-editor-container .world-editor-entry-order,
|
||||
#world-editor-container .world-editor-entry-row > div[data-label="条目"] {
|
||||
grid-column: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Truncation for keys and content */
|
||||
#world-editor-container .world-editor-entry-keys,
|
||||
#world-editor-container .world-editor-entry-content {
|
||||
white-space: normal;
|
||||
max-width: 100%;
|
||||
display: -webkit-box !important;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Ensure inline edits take full width on mobile */
|
||||
#world-editor-container .inline-edit {
|
||||
width: calc(100% - 60px); /* Adjust for label width roughly */
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-batch-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#world-editor-container .world-editor-batch-actions .world-editor-btn {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
813
WorldEditor/WorldEditor.js
Normal file
813
WorldEditor/WorldEditor.js
Normal file
@@ -0,0 +1,813 @@
|
||||
|
||||
import { world_names, loadWorldInfo, saveWorldInfo, deleteWorldInfo, updateWorldInfoList } 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';
|
||||
import { safeLorebooks, safeLorebookEntries, safeUpdateLorebookEntries, compatibleWriteToLorebook } from '../core/tavernhelper-compatibility.js';
|
||||
import { amilyHelper } from '../core/tavern-helper/main.js';
|
||||
import { escapeHTML } from '../utils/utils.js';
|
||||
const { SillyTavern } = window;
|
||||
|
||||
class WorldEditor {
|
||||
constructor() {
|
||||
this.isLoading = false;
|
||||
|
||||
this.allWorldBooks = [];
|
||||
this.filteredWorldBooks = [];
|
||||
this.selectedWorldBooks = new Set();
|
||||
|
||||
this.currentWorldBook = null;
|
||||
this.entries = [];
|
||||
this.selectedEntries = new Set();
|
||||
this.filteredEntries = [];
|
||||
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-book-selection-view', 'world-editor-entry-view',
|
||||
// 顶部按钮
|
||||
'world-editor-refresh-btn', 'world-editor-create-book-btn', 'world-editor-create-entry-btn',
|
||||
// 世界书视图
|
||||
'world-book-search-box', 'world-book-search-btn', 'world-book-count',
|
||||
'world-book-batch-actions', 'world-book-selected-count', 'world-book-clone-btn', 'world-book-delete-btn',
|
||||
'world-book-list-container',
|
||||
// 条目视图
|
||||
'world-editor-current-book-title', 'world-editor-back-to-list-btn',
|
||||
'world-editor-search-type', '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-copy-entries-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]) {
|
||||
console.warn(`[世界书编辑器] UI元素缺失: ${id}`);
|
||||
if (id.endsWith('container') || id.endsWith('view')) {
|
||||
missing = true; // 关键元素缺失
|
||||
}
|
||||
}
|
||||
}
|
||||
return !missing;
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 视图切换
|
||||
this.elements.worldEditorBackToListBtn.addEventListener('click', () => this.switchToBookListView());
|
||||
|
||||
// 顶部按钮
|
||||
this.elements.worldEditorRefreshBtn.addEventListener('click', () => this.loadAvailableWorldBooks());
|
||||
this.elements.worldEditorCreateBookBtn.addEventListener('click', () => this.createNewWorldBook());
|
||||
this.elements.worldEditorCreateEntryBtn.addEventListener('click', () => this.openCreateModal());
|
||||
|
||||
// 世界书视图事件
|
||||
this.elements.worldBookSearchBox.addEventListener('input', () => this.filterWorldBooks());
|
||||
this.elements.worldBookSearchBtn.addEventListener('click', () => this.filterWorldBooks());
|
||||
this.elements.worldBookCloneBtn.addEventListener('click', () => this.cloneSelectedBooks());
|
||||
this.elements.worldBookDeleteBtn.addEventListener('click', () => this.deleteSelectedBooks());
|
||||
|
||||
// 条目视图事件
|
||||
document.querySelector('#world-editor-entry-view .world-editor-entries-header').addEventListener('click', (e) => {
|
||||
if (e.target.dataset.sort) this.sortEntries(e.target.dataset.sort);
|
||||
});
|
||||
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.worldEditorCopyEntriesBtn.addEventListener('click', () => this.copySelectedEntries());
|
||||
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', '防止递归'));
|
||||
}
|
||||
|
||||
// 视图管理
|
||||
switchToBookListView() {
|
||||
this.elements.worldBookSelectionView.style.display = 'block';
|
||||
this.elements.worldEditorEntryView.style.display = 'none';
|
||||
this.elements.worldEditorCreateEntryBtn.disabled = true;
|
||||
this.currentWorldBook = null;
|
||||
}
|
||||
|
||||
switchToEntryView(bookName) {
|
||||
this.elements.worldBookSelectionView.style.display = 'none';
|
||||
this.elements.worldEditorEntryView.style.display = 'block';
|
||||
this.elements.worldEditorCreateEntryBtn.disabled = false;
|
||||
this.elements.worldEditorCurrentBookTitle.textContent = `当前编辑:${bookName}`;
|
||||
this.loadWorldBookEntries(bookName);
|
||||
}
|
||||
|
||||
// 世界书数据处理
|
||||
async loadAvailableWorldBooks() {
|
||||
this.setLoading(true);
|
||||
try {
|
||||
const books = await this.getAllWorldBooks();
|
||||
this.allWorldBooks = books.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.filterWorldBooks(); // 这会渲染列表
|
||||
} catch (error) {
|
||||
this.showError('加载世界书列表失败: ' + error.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllWorldBooks() {
|
||||
const books = await safeLorebooks();
|
||||
return books.map(name => ({ name }));
|
||||
}
|
||||
|
||||
filterWorldBooks() {
|
||||
const term = this.elements.worldBookSearchBox.value.toLowerCase();
|
||||
this.filteredWorldBooks = this.allWorldBooks.filter(book => book.name.toLowerCase().includes(term));
|
||||
this.renderWorldBookList();
|
||||
this.updateWorldBookCount();
|
||||
}
|
||||
|
||||
renderWorldBookList() {
|
||||
const container = this.elements.worldBookListContainer;
|
||||
container.innerHTML = ''; // 清空
|
||||
if (this.filteredWorldBooks.length === 0) {
|
||||
container.innerHTML = '<p class="world-editor-empty-state">没有找到世界书</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
this.filteredWorldBooks.forEach(book => {
|
||||
const isSelected = this.selectedWorldBooks.has(book.name);
|
||||
const row = document.createElement('div');
|
||||
row.className = `world-book-row ${isSelected ? 'selected' : ''}`;
|
||||
row.dataset.bookName = book.name;
|
||||
row.innerHTML = `
|
||||
<input type="checkbox" class="world-book-checkbox" ${isSelected ? 'checked' : ''}>
|
||||
<span class="world-book-name">${escapeHTML(book.name)}</span>
|
||||
<div class="world-book-actions">
|
||||
<button class="world-editor-btn small-btn" data-action="edit"><i class="fas fa-pencil-alt"></i> 编辑</button>
|
||||
<button class="world-editor-btn small-btn" data-action="rename"><i class="fas fa-i-cursor"></i> 重命名</button>
|
||||
</div>
|
||||
`;
|
||||
fragment.appendChild(row);
|
||||
});
|
||||
container.appendChild(fragment);
|
||||
this.bindWorldBookListEvents();
|
||||
}
|
||||
|
||||
bindWorldBookListEvents() {
|
||||
this.elements.worldBookListContainer.querySelectorAll('.world-book-row').forEach(row => {
|
||||
const bookName = row.dataset.bookName;
|
||||
// 复选框事件
|
||||
row.querySelector('.world-book-checkbox').addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
this.selectedWorldBooks.add(bookName);
|
||||
} else {
|
||||
this.selectedWorldBooks.delete(bookName);
|
||||
}
|
||||
row.classList.toggle('selected', e.target.checked);
|
||||
this.updateWorldBookSelectionUI();
|
||||
});
|
||||
|
||||
// 按钮事件
|
||||
row.querySelector('[data-action="edit"]').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.switchToEntryView(bookName);
|
||||
});
|
||||
row.querySelector('[data-action="rename"]').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.renameWorldBook(bookName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async createNewWorldBook() {
|
||||
const bookName = prompt("请输入新的世界书名称:");
|
||||
if (bookName && bookName.trim()) {
|
||||
const trimmedBookName = bookName.trim();
|
||||
try {
|
||||
await compatibleWriteToLorebook(trimmedBookName, '新条目', () => '这是一个新条目', {});
|
||||
if (window.toastr) window.toastr.success(`世界书 "${trimmedBookName}" 创建成功!`);
|
||||
this.loadAvailableWorldBooks();
|
||||
} catch (error) {
|
||||
this.showError(`创建失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async renameWorldBook(oldName) {
|
||||
const newName = prompt(`重命名世界书 "${oldName}":`, oldName);
|
||||
if (newName && newName.trim() && newName !== oldName) {
|
||||
const trimmedNewName = newName.trim();
|
||||
try {
|
||||
const bookData = await loadWorldInfo(oldName);
|
||||
await saveWorldInfo(trimmedNewName, bookData);
|
||||
await deleteWorldInfo(oldName);
|
||||
if (window.toastr) window.toastr.success('重命名成功!');
|
||||
|
||||
await updateWorldInfoList();
|
||||
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
|
||||
this.loadAvailableWorldBooks();
|
||||
} catch (error) {
|
||||
this.showError(`重命名失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async cloneSelectedBooks() {
|
||||
if (this.selectedWorldBooks.size === 0) return;
|
||||
if (!confirm(`确定要为 ${this.selectedWorldBooks.size} 个世界书创建备份吗?`)) return;
|
||||
|
||||
this.setLoading(true);
|
||||
try {
|
||||
for (const bookName of this.selectedWorldBooks) {
|
||||
const newName = `${bookName}_备份_${Date.now()}`;
|
||||
const bookData = await loadWorldInfo(bookName);
|
||||
await saveWorldInfo(newName, bookData);
|
||||
}
|
||||
if (window.toastr) window.toastr.success('备份创建成功!');
|
||||
|
||||
await updateWorldInfoList();
|
||||
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
|
||||
this.loadAvailableWorldBooks();
|
||||
} catch (error) {
|
||||
this.showError(`备份失败: ${error.message}`);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSelectedBooks() {
|
||||
if (this.selectedWorldBooks.size === 0) return;
|
||||
if (!confirm(`警告:这将永久删除 ${this.selectedWorldBooks.size} 个世界书及其所有内容!确定要继续吗?`)) return;
|
||||
|
||||
this.setLoading(true);
|
||||
try {
|
||||
for (const bookName of this.selectedWorldBooks) {
|
||||
await deleteWorldInfo(bookName);
|
||||
}
|
||||
if (window.toastr) window.toastr.success('批量删除成功!');
|
||||
|
||||
await updateWorldInfoList();
|
||||
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
|
||||
this.loadAvailableWorldBooks();
|
||||
} catch (error) {
|
||||
this.showError(`删除失败: ${error.message}`);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
updateWorldBookCount() {
|
||||
this.elements.worldBookCount.textContent = `世界书:${this.allWorldBooks.length}`;
|
||||
}
|
||||
|
||||
updateWorldBookSelectionUI() {
|
||||
const count = this.selectedWorldBooks.size;
|
||||
this.elements.worldBookSelectedCount.textContent = `已选择 ${count} 项`;
|
||||
this.elements.worldBookBatchActions.classList.toggle('active', count > 0);
|
||||
}
|
||||
|
||||
|
||||
// 条目数据处理 (大部分逻辑与旧版相似)
|
||||
async loadWorldBookEntries(worldBookName) {
|
||||
if (!worldBookName) {
|
||||
this.entries = [];
|
||||
this.filteredEntries = [];
|
||||
this.selectedEntries.clear();
|
||||
this.renderEntries();
|
||||
this.updateEntryCount();
|
||||
this.updateSelectionUI();
|
||||
return;
|
||||
}
|
||||
this.setLoading(true);
|
||||
this.currentWorldBook = worldBookName;
|
||||
try {
|
||||
const bookData = await loadWorldInfo(worldBookName);
|
||||
if (!bookData || !bookData.entries) {
|
||||
this.entries = [];
|
||||
this.filteredEntries = [];
|
||||
this.renderEntries();
|
||||
this.updateEntryCount();
|
||||
return;
|
||||
}
|
||||
|
||||
const positionMap = {
|
||||
0: 'before_character_definition',
|
||||
1: 'after_character_definition',
|
||||
2: 'before_author_note',
|
||||
3: 'after_author_note',
|
||||
4: 'at_depth'
|
||||
};
|
||||
|
||||
this.entries = Object.entries(bookData.entries).map(([uid, e]) => ({
|
||||
uid: parseInt(uid),
|
||||
enabled: !e.disable,
|
||||
type: e.constant ? 'constant' : 'selective',
|
||||
keys: e.key || [],
|
||||
content: e.content || '',
|
||||
position: positionMap[e.position] || 'at_depth',
|
||||
depth: e.depth != null ? e.depth : 4,
|
||||
order: e.order != null ? e.order : 100,
|
||||
comment: e.comment || '',
|
||||
exclude_recursion: e.excludeRecursion || false,
|
||||
prevent_recursion: e.preventRecursion || false
|
||||
}));
|
||||
|
||||
this.filteredEntries = [...this.entries];
|
||||
this.renderEntries();
|
||||
this.updateEntryCount();
|
||||
} catch (error) {
|
||||
this.showError(`加载条目失败: ${error.message}`);
|
||||
this.entries = [];
|
||||
this.filteredEntries = [];
|
||||
} finally {
|
||||
this.selectedEntries.clear();
|
||||
this.updateSelectionUI();
|
||||
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');
|
||||
|
||||
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 = '<p>没有条目</p>';
|
||||
container.appendChild(emptyState);
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
this.filteredEntries.forEach(e => {
|
||||
const rowHTML = this.renderEntryRow(e).trim();
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = rowHTML;
|
||||
const rowElement = tempDiv.firstChild;
|
||||
|
||||
const contentCell = rowElement.querySelector('.world-editor-entry-content');
|
||||
if (contentCell) {
|
||||
contentCell.textContent = e.content || '';
|
||||
}
|
||||
|
||||
fragment.appendChild(rowElement);
|
||||
});
|
||||
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 = `<select class="inline-edit" data-field="position" data-uid="${entry.uid}">
|
||||
${Object.entries(positionOptions).map(([value, text]) => `<option value="${value}" ${entry.position === value ? 'selected' : ''}>${text}</option>`).join('')}
|
||||
</select>`;
|
||||
|
||||
return `
|
||||
<div class="world-editor-entry-row ${this.selectedEntries.has(entry.uid) ? 'selected' : ''}" data-uid="${entry.uid}">
|
||||
<div data-label="选择"><input type="checkbox" class="world-editor-entry-checkbox" ${this.selectedEntries.has(entry.uid) ? 'checked' : ''}></div>
|
||||
<div data-label="状态" class="inline-toggle" data-field="enabled" data-uid="${entry.uid}"><i class="fas ${entry.enabled ? 'fa-toggle-on' : 'fa-toggle-off'}"></i></div>
|
||||
<div data-label="灯色" class="inline-toggle" data-field="type" data-uid="${entry.uid}">${entry.type === 'constant' ? '🔵' : '🟢'}</div>
|
||||
<div data-label="条目"><input type="text" class="inline-edit" data-field="comment" data-uid="${entry.uid}" value="${escapeHTML(entry.comment || '')}" placeholder="点击填写条目名"></div>
|
||||
<div data-label="内容" class="world-editor-entry-content" data-action="open-editor" data-uid="${entry.uid}" title="${escapeHTML(entry.content || '')}">${escapeHTML(entry.content || '')}</div>
|
||||
<div data-label="位置">${positionSelect}</div>
|
||||
<div data-label="深度"><input type="number" class="inline-edit" data-field="depth" data-uid="${entry.uid}" value="${entry.depth != null ? entry.depth : ''}" ${!String(entry.position)?.startsWith('at_depth') ? 'disabled' : ''}></div>
|
||||
<div data-label="顺序"><input type="number" class="inline-edit" data-field="order" data-uid="${entry.uid}" value="${entry.order}"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
bindEntryEvents() {
|
||||
this.elements.worldEditorEntriesContainer.querySelectorAll('.world-editor-entry-row').forEach(row => {
|
||||
const uid = parseInt(row.dataset.uid);
|
||||
const checkbox = row.querySelector('.world-editor-entry-checkbox');
|
||||
checkbox.addEventListener('change', (e) => {
|
||||
if (e.target.checked) this.selectedEntries.add(uid); else this.selectedEntries.delete(uid);
|
||||
row.classList.toggle('selected', e.target.checked);
|
||||
this.updateSelectionUI();
|
||||
});
|
||||
row.querySelector('[data-action="open-editor"]').addEventListener('click', () => this.openEditModal(uid));
|
||||
row.querySelectorAll('.inline-toggle').forEach(toggle => {
|
||||
toggle.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const field = toggle.dataset.field;
|
||||
const entry = this.entries.find(e => e.uid === uid);
|
||||
let newValue;
|
||||
if (field === 'enabled') newValue = !entry.enabled;
|
||||
if (field === 'type') newValue = entry.type === 'constant' ? 'selective' : 'constant';
|
||||
this.updateSingleEntry(uid, { [field]: newValue });
|
||||
});
|
||||
});
|
||||
row.querySelectorAll('.inline-edit').forEach(input => {
|
||||
input.addEventListener('change', (e) => {
|
||||
e.stopPropagation();
|
||||
const field = input.dataset.field;
|
||||
let value = input.value;
|
||||
if (input.type === 'number') value = parseInt(value, 10);
|
||||
const updates = { [field]: value };
|
||||
if (field === 'position') {
|
||||
const depthInput = row.querySelector('[data-field="depth"]');
|
||||
if (depthInput) depthInput.disabled = !value.startsWith('at_depth');
|
||||
}
|
||||
this.updateSingleEntry(uid, updates);
|
||||
});
|
||||
input.addEventListener('click', e => e.stopPropagation());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用原生 saveWorldInfo 更新条目,避免界面跳转
|
||||
* @param {Array<object>} entriesToUpdate - 需要更新的条目对象数组
|
||||
*/
|
||||
async updateEntriesWithNativeMethod(entriesToUpdate) {
|
||||
try {
|
||||
// 将所有更新逻辑统一到 amilyHelper.setLorebookEntries
|
||||
await amilyHelper.setLorebookEntries(this.currentWorldBook, entriesToUpdate);
|
||||
|
||||
// Optimistic UI update in local state
|
||||
for (const updatedEntry of entriesToUpdate) {
|
||||
const localEntry = this.entries.find(e => e.uid === updatedEntry.uid);
|
||||
if (localEntry) {
|
||||
Object.assign(localEntry, updatedEntry);
|
||||
}
|
||||
}
|
||||
this.renderEntries();
|
||||
|
||||
} catch (error) {
|
||||
this.showError(`更新失败: ${error.message}`);
|
||||
this.loadWorldBookEntries(this.currentWorldBook); // On error, re-sync with truth
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert string position to native number format
|
||||
convertPositionToNative(posStr) {
|
||||
const map = {
|
||||
'before_character_definition': 0,
|
||||
'after_character_definition': 1,
|
||||
'before_author_note': 2,
|
||||
'after_author_note': 3,
|
||||
'at_depth': 4,
|
||||
'at_depth_as_system': 4
|
||||
};
|
||||
return map[posStr] !== undefined ? map[posStr] : 4;
|
||||
}
|
||||
|
||||
async updateSingleEntry(uid, updates) {
|
||||
const entry = this.entries.find(e => e.uid === uid);
|
||||
if (!entry) return;
|
||||
const updatedEntry = { ...entry, ...updates };
|
||||
await this.updateEntriesWithNativeMethod([updatedEntry]);
|
||||
}
|
||||
|
||||
async batchUpdateEntries(updates, confirmation = null) {
|
||||
if (this.selectedEntries.size === 0) return;
|
||||
if (confirmation && !confirm(confirmation)) return;
|
||||
|
||||
const entriesToUpdate = this.entries
|
||||
.filter(e => this.selectedEntries.has(e.uid))
|
||||
.map(e => ({ ...e, ...updates }));
|
||||
|
||||
await this.updateEntriesWithNativeMethod(entriesToUpdate);
|
||||
if (window.toastr) window.toastr.success('批量更新成功!');
|
||||
}
|
||||
|
||||
toggleBatchRecursion(field, fieldName) {
|
||||
if (this.selectedEntries.size === 0) return;
|
||||
const selected = this.entries.filter(e => this.selectedEntries.has(e.uid));
|
||||
const enabledCount = selected.filter(e => e[field]).length;
|
||||
const shouldEnable = enabledCount <= selected.length / 2;
|
||||
const action = shouldEnable ? '启用' : '禁用';
|
||||
const confirmation = `确定为 ${this.selectedEntries.size} 个条目 ${action} "${fieldName}" 吗?`;
|
||||
this.batchUpdateEntries({ [field]: shouldEnable }, confirmation);
|
||||
}
|
||||
|
||||
async copySelectedEntries() {
|
||||
if (this.selectedEntries.size === 0) {
|
||||
this.showError('请先选择要复制的条目');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有世界书列表(包括当前世界书,允许在同一世界书内复制)
|
||||
const availableBooks = this.allWorldBooks.map(book => book.name);
|
||||
|
||||
if (availableBooks.length === 0) {
|
||||
this.showError('没有可用的世界书');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[世界书编辑器] 准备复制条目,已选择:', this.selectedEntries.size, '个条目');
|
||||
console.log('[世界书编辑器] 选中的UID:', Array.from(this.selectedEntries));
|
||||
|
||||
// 创建选择对话框
|
||||
const selectHtml = `
|
||||
<style>
|
||||
.copy-dialog { padding: 20px; }
|
||||
.copy-dialog label { display: block; margin-bottom: 10px; color: #ccc; font-weight: bold; }
|
||||
.copy-dialog select { width: 100%; padding: 10px; background-color: #404040; color: white; border: 1px solid #555; border-radius: 4px; font-size: 14px; }
|
||||
.copy-dialog .info { margin-top: 15px; padding: 10px; background-color: #2a2a2a; border-left: 3px solid #4a9eff; color: #ccc; }
|
||||
</style>
|
||||
<div class="copy-dialog">
|
||||
<label for="target-worldbook">选择目标世界书:</label>
|
||||
<select id="target-worldbook" class="form-control">
|
||||
${availableBooks.map(name => `<option value="${escapeHTML(name)}" ${name === this.currentWorldBook ? 'selected' : ''}>${escapeHTML(name)}${name === this.currentWorldBook ? ' (当前)' : ''}</option>`).join('')}
|
||||
</select>
|
||||
<div class="info">
|
||||
将复制 ${this.selectedEntries.size} 个条目到目标世界书
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
showHtmlModal('复制条目', selectHtml, {
|
||||
onOk: async (dialog) => {
|
||||
const targetBook = dialog.find('#target-worldbook').val();
|
||||
|
||||
if (!targetBook) {
|
||||
this.showError('请选择目标世界书');
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.performCopy(targetBook);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async performCopy(targetBookName) {
|
||||
this.setLoading(true);
|
||||
try {
|
||||
// 获取要复制的条目
|
||||
const entriesToCopy = this.entries.filter(e => this.selectedEntries.has(e.uid));
|
||||
|
||||
console.log('[世界书编辑器] 过滤后的条目数量:', entriesToCopy.length);
|
||||
console.log('[世界书编辑器] 条目详情:', entriesToCopy);
|
||||
|
||||
if (entriesToCopy.length === 0) {
|
||||
this.showError('没有选中的条目');
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载目标世界书
|
||||
const targetBookData = await loadWorldInfo(targetBookName);
|
||||
if (!targetBookData) {
|
||||
this.showError(`目标世界书 "${targetBookName}" 不存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备要创建的条目数据
|
||||
const newEntries = entriesToCopy.map(entry => ({
|
||||
enabled: entry.enabled,
|
||||
type: entry.type,
|
||||
keys: Array.isArray(entry.keys) ? entry.keys : [],
|
||||
content: entry.content || '',
|
||||
position: entry.position,
|
||||
depth: entry.depth != null ? entry.depth : 4,
|
||||
order: entry.order != null ? entry.order : 100,
|
||||
comment: entry.comment || '',
|
||||
exclude_recursion: entry.exclude_recursion || false,
|
||||
prevent_recursion: entry.prevent_recursion || false
|
||||
}));
|
||||
|
||||
console.log('[世界书编辑器] 准备创建的条目:', newEntries);
|
||||
|
||||
// 在目标世界书中创建条目
|
||||
await amilyHelper.createLorebookEntries(targetBookName, newEntries);
|
||||
|
||||
if (window.toastr) {
|
||||
window.toastr.success(`成功复制 ${entriesToCopy.length} 个条目到 "${targetBookName}"`);
|
||||
}
|
||||
|
||||
// 如果复制到当前世界书,刷新视图
|
||||
if (targetBookName === this.currentWorldBook) {
|
||||
await this.loadWorldBookEntries(this.currentWorldBook);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[世界书编辑器] 复制失败:', error);
|
||||
this.showError(`复制失败: ${error.message}`);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async batchDeleteEntries() {
|
||||
if (this.selectedEntries.size === 0 || !confirm(`删除 ${this.selectedEntries.size} 个条目?`)) return;
|
||||
try {
|
||||
const bookData = await loadWorldInfo(this.currentWorldBook);
|
||||
if (!bookData) throw new Error(`World book "${this.currentWorldBook}" not found.`);
|
||||
this.selectedEntries.forEach(uid => {
|
||||
delete bookData.entries[uid];
|
||||
});
|
||||
await saveWorldInfo(this.currentWorldBook, bookData, true);
|
||||
this.loadWorldBookEntries(this.currentWorldBook);
|
||||
} catch (error) {
|
||||
this.showError(`删除失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectAll(checked) {
|
||||
this.selectedEntries.clear();
|
||||
if (checked) this.filteredEntries.forEach(e => this.selectedEntries.add(e.uid));
|
||||
this.renderEntries();
|
||||
this.updateSelectionUI();
|
||||
}
|
||||
|
||||
updateSelectionUI() {
|
||||
const count = this.selectedEntries.size;
|
||||
this.elements.worldEditorSelectedCount.textContent = `已选择 ${count} 项`;
|
||||
this.elements.worldEditorBatchActions.classList.toggle('active', count > 0);
|
||||
this.elements.worldEditorSelectAll.checked = count > 0 && count === this.filteredEntries.length;
|
||||
this.elements.worldEditorSelectAll.indeterminate = count > 0 && count < this.filteredEntries.length;
|
||||
}
|
||||
|
||||
updateEntryCount() { this.elements.worldEditorEntryCount.textContent = `条目:${this.entries.length}`; }
|
||||
|
||||
filterEntries() {
|
||||
const term = this.elements.worldEditorSearchBox.value.toLowerCase();
|
||||
const searchType = this.elements.worldEditorSearchType.value;
|
||||
this.filteredEntries = !term ? [...this.entries] : this.entries.filter(e => (e[searchType] || '').toLowerCase().includes(term));
|
||||
this.renderEntries();
|
||||
}
|
||||
|
||||
openCreateModal() {
|
||||
this.currentEditingEntry = null;
|
||||
const entry = { enabled: true, type: 'selective', keys: [], content: '', position: 'at_depth', depth: 4, order: 100, comment: '' };
|
||||
this.showEditModal('创建新条目', entry);
|
||||
}
|
||||
|
||||
openEditModal(uid) {
|
||||
const entry = this.entries.find(e => e.uid === uid);
|
||||
if (!entry) return;
|
||||
this.currentEditingEntry = entry;
|
||||
this.showEditModal('编辑条目', entry);
|
||||
}
|
||||
|
||||
showEditModal(title, entry) {
|
||||
const formHtml = this.getEditFormHtml(entry);
|
||||
showHtmlModal(title, formHtml, { onOk: (d) => { this.saveEntry(d); return true; } });
|
||||
}
|
||||
|
||||
getEditFormHtml(entry) {
|
||||
return `
|
||||
<style>
|
||||
.world-editor-form-grid { display: grid; grid-template-columns: 120px 1fr; gap: 15px; align-items: center; }
|
||||
.world-editor-form-grid label { text-align: right; color: #ccc; }
|
||||
.world-editor-form-grid .form-control { width: 100%; padding: 8px; background-color: #404040; color: white; border: 1px solid #555; border-radius: 4px; box-sizing: border-box; }
|
||||
.world-editor-form-grid textarea.form-control { min-height: 100px; resize: vertical; }
|
||||
.world-editor-form-grid .full-width { grid-column: 1 / -1; }
|
||||
.world-editor-form-grid .checkbox-group { grid-column: 2 / -1; display: flex; align-items: center; gap: 8px; }
|
||||
</style>
|
||||
<form id="world-editor-edit-form" class="world-editor-form-grid">
|
||||
<div class="checkbox-group"><input type="checkbox" id="world-editor-entry-enabled" ${entry.enabled ? 'checked' : ''}><label for="world-editor-entry-enabled">启用条目</label></div>
|
||||
<label for="world-editor-entry-type">激活模式:</label><select id="world-editor-entry-type" class="form-control"><option value="selective" ${entry.type === 'selective' ? 'selected' : ''}>🟢 绿灯 (关键词触发)</option><option value="constant" ${entry.type === 'constant' ? 'selected' : ''}>🔵 蓝灯 (始终激活)</option></select>
|
||||
<label for="world-editor-entry-keys" class="full-width" style="text-align: left; grid-column: 1 / -1;">关键词 (每行一个):</label><textarea id="world-editor-entry-keys" class="form-control full-width" placeholder="输入关键词,每行一个">${(entry.keys || []).join('\n')}</textarea>
|
||||
<label for="world-editor-entry-content" class="full-width" style="text-align: left; grid-column: 1 / -1;">内容:</label><textarea id="world-editor-entry-content" class="form-control full-width" placeholder="输入条目内容">${entry.content || ''}</textarea>
|
||||
<label for="world-editor-entry-position">插入位置:</label><select id="world-editor-entry-position" class="form-control"><option value="before_character_definition" ${entry.position === 'before_character_definition' ? 'selected' : ''}>角色定义之前</option><option value="after_character_definition" ${entry.position === 'after_character_definition' ? 'selected' : ''}>角色定义之后</option><option value="before_author_note" ${entry.position === 'before_author_note' ? 'selected' : ''}>作者注释之前</option><option value="after_author_note" ${entry.position === 'after_author_note' ? 'selected' : ''}>作者注释之后</option><option value="at_depth" ${entry.position === 'at_depth' ? 'selected' : ''}>@D 注入指定深度</option></select>
|
||||
<label for="world-editor-entry-depth">深度:</label><input type="number" id="world-editor-entry-depth" class="form-control" min="0" max="9999" value="${entry.depth || 4}">
|
||||
<label for="world-editor-entry-order">顺序:</label><input type="number" id="world-editor-entry-order" class="form-control" min="0" max="9999" value="${entry.order || 100}">
|
||||
<label for="world-editor-entry-comment">备注:</label><input type="text" id="world-editor-entry-comment" class="form-control" placeholder="可选的备注信息" value="${entry.comment || ''}">
|
||||
<div class="checkbox-group"><input type="checkbox" id="world-editor-entry-disable-recursion" ${entry.exclude_recursion ? 'checked' : ''}><label for="world-editor-entry-disable-recursion">不可递归 (不会被其他条目激活)</label></div>
|
||||
<div class="checkbox-group"><input type="checkbox" id="world-editor-entry-prevent-recursion" ${entry.prevent_recursion ? 'checked' : ''}><label for="world-editor-entry-prevent-recursion">防止进一步递归 (本条目将不会激活其他条目)</label></div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
async saveEntry(dialog) {
|
||||
const formData = this.getFormDataFromModal(dialog);
|
||||
try {
|
||||
if (this.currentEditingEntry) {
|
||||
// 使用改造后的原生方法更新
|
||||
await this.updateEntriesWithNativeMethod([{ ...this.currentEditingEntry, ...formData }]);
|
||||
} else {
|
||||
// 创建条目仍然可以使用TavernHelper,因为它通常不会触发跳转
|
||||
await amilyHelper.createLorebookEntries(this.currentWorldBook, [formData]);
|
||||
}
|
||||
// 刷新当前视图
|
||||
this.loadWorldBookEntries(this.currentWorldBook);
|
||||
} catch (error) {
|
||||
this.showError(`保存失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
getFormDataFromModal(dialog) {
|
||||
return {
|
||||
enabled: dialog.find('#world-editor-entry-enabled').is(':checked'),
|
||||
type: dialog.find('#world-editor-entry-type').val(),
|
||||
keys: dialog.find('#world-editor-entry-keys').val().split('\n').map(k => k.trim()).filter(Boolean),
|
||||
content: dialog.find('#world-editor-entry-content').val(),
|
||||
position: dialog.find('#world-editor-entry-position').val(),
|
||||
depth: parseInt(dialog.find('#world-editor-entry-depth').val()),
|
||||
order: parseInt(dialog.find('#world-editor-entry-order').val()),
|
||||
comment: dialog.find('#world-editor-entry-comment').val(),
|
||||
exclude_recursion: dialog.find('#world-editor-entry-disable-recursion').is(':checked'),
|
||||
prevent_recursion: dialog.find('#world-editor-entry-prevent-recursion').is(':checked')
|
||||
};
|
||||
}
|
||||
|
||||
setLoading(loading) {
|
||||
this.isLoading = loading;
|
||||
document.getElementById('world-editor-container').classList.toggle('loading', loading);
|
||||
}
|
||||
showError(msg) { if (window.toastr) window.toastr.error(msg); console.error(msg); }
|
||||
|
||||
sortEntries(key) {
|
||||
if (this.sortState.key === key) this.sortState.asc = !this.sortState.asc;
|
||||
else { this.sortState.key = key; this.sortState.asc = true; }
|
||||
this.renderEntries();
|
||||
}
|
||||
|
||||
sortFilteredEntries() {
|
||||
const { key, asc } = this.sortState;
|
||||
this.filteredEntries.sort((a, b) => {
|
||||
let valA = a[key], valB = b[key];
|
||||
if (typeof valA === 'string') valA = valA.toLowerCase();
|
||||
if (typeof valB === 'string') valB = valB.toLowerCase();
|
||||
if (valA < valB) return asc ? -1 : 1;
|
||||
if (valA > valB) return asc ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
bindExternalEvents() {
|
||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||
console.log('[世界书编辑器] 检测到聊天变更,将自动刷新。');
|
||||
if (this.currentWorldBook) {
|
||||
this.loadWorldBookEntries(this.currentWorldBook);
|
||||
} else {
|
||||
this.loadAvailableWorldBooks();
|
||||
}
|
||||
});
|
||||
console.log('[世界书编辑器] 已成功绑定外部事件监听器。');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeWorldEditor() {
|
||||
const panel = document.getElementById('amily2_world_editor_panel');
|
||||
if (!panel) {
|
||||
console.error('[WorldEditor] Panel not found, initialization aborted.');
|
||||
return;
|
||||
}
|
||||
if (panel.dataset.initialized) {
|
||||
return;
|
||||
}
|
||||
panel.dataset.initialized = 'true';
|
||||
console.log('[WorldEditor] Initializing WorldEditor instance.');
|
||||
window.worldEditorInstance = new WorldEditor();
|
||||
}
|
||||
|
||||
function tryInitialize() {
|
||||
const panel = document.getElementById('amily2_world_editor_panel');
|
||||
if (panel) {
|
||||
initializeWorldEditor();
|
||||
} else {
|
||||
const observer = new MutationObserver((mutations, obs) => {
|
||||
const panel = document.getElementById('amily2_world_editor_panel');
|
||||
if (panel) {
|
||||
obs.disconnect();
|
||||
initializeWorldEditor();
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', tryInitialize);
|
||||
} else {
|
||||
tryInitialize();
|
||||
}
|
||||
Reference in New Issue
Block a user