mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 18:15:50 +00:00
1494 lines
60 KiB
JavaScript
1494 lines
60 KiB
JavaScript
|
||
|
||
import * as TableManager from '../core/table-system/manager.js';
|
||
import { log } from '../core/table-system/logger.js';
|
||
import { extension_settings, getContext } from '/scripts/extensions.js';
|
||
import { extensionName } from '../utils/settings.js';
|
||
import { saveSettingsDebounced } from '/script.js';
|
||
import { startBatchFilling } from '../core/table-system/batch-filler.js';
|
||
import { DEFAULT_AI_RULE_TEMPLATE, DEFAULT_AI_FLOW_TEMPLATE } from '../core/table-system/settings.js';
|
||
import { world_names, loadWorldInfo } from '/scripts/world-info.js';
|
||
import { safeCharLorebooks, safeLorebookEntries } from '../core/tavernhelper-compatibility.js';
|
||
import { characters, this_chid, eventSource, event_types } from "/script.js";
|
||
import { fetchNccsModels, testNccsApiConnection } from '../core/api/NccsApi.js';
|
||
|
||
const getAllTablesContainer = () => document.getElementById('all-tables-container');
|
||
|
||
|
||
function toggleRowContextMenu(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
|
||
const targetTd = event.target.closest('td.index-col');
|
||
if (!targetTd) return;
|
||
|
||
const menu = targetTd.querySelector('.amily2-context-menu');
|
||
if (!menu) return;
|
||
|
||
const isActive = menu.classList.contains('amily2-menu-active');
|
||
|
||
document.querySelectorAll('.amily2-context-menu.amily2-menu-active').forEach(activeMenu => {
|
||
activeMenu.classList.remove('amily2-menu-active');
|
||
});
|
||
|
||
if (!isActive) {
|
||
menu.classList.add('amily2-menu-active');
|
||
}
|
||
|
||
const closeMenu = (e) => {
|
||
if (!menu.contains(e.target)) {
|
||
menu.classList.remove('amily2-menu-active');
|
||
document.removeEventListener('click', closeMenu, true);
|
||
}
|
||
};
|
||
|
||
setTimeout(() => {
|
||
if (menu.classList.contains('amily2-menu-active')) {
|
||
document.addEventListener('click', closeMenu, true);
|
||
}
|
||
}, 0);
|
||
}
|
||
|
||
|
||
function toggleColumnContextMenu(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
|
||
const targetTh = event.target.closest('th');
|
||
if (!targetTh) return;
|
||
|
||
const isActive = targetTh.classList.contains('amily2-menu-open');
|
||
|
||
document.querySelectorAll('th.amily2-menu-open').forEach(openTh => {
|
||
openTh.classList.remove('amily2-menu-open');
|
||
});
|
||
|
||
if (!isActive) {
|
||
targetTh.classList.add('amily2-menu-open');
|
||
}
|
||
|
||
const closeMenu = (e) => {
|
||
if (!targetTh.contains(e.target)) {
|
||
targetTh.classList.remove('amily2-menu-open');
|
||
document.removeEventListener('click', closeMenu, true);
|
||
}
|
||
};
|
||
|
||
setTimeout(() => {
|
||
if (targetTh.classList.contains('amily2-menu-open')) {
|
||
document.addEventListener('click', closeMenu, true);
|
||
}
|
||
}, 0);
|
||
}
|
||
|
||
|
||
function showInputDialog({ title, label, currentValue, placeholder, onSave }) {
|
||
const dialogHtml = `
|
||
<dialog class="popup custom-input-dialog">
|
||
<div class="popup-body">
|
||
<h4 style="margin-top:0; color: #e0e0e0; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 10px; display: flex; align-items: center; gap: 8px;">
|
||
<i class="fas fa-edit" style="color: #9e8aff;"></i> ${title}
|
||
</h4>
|
||
<div class="popup-content" style="padding: 20px 10px;">
|
||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||
<label style="color: #ccc; font-weight: bold;">${label}</label>
|
||
<input type="text" id="generic-input" class="text_pole"
|
||
value="${currentValue}"
|
||
style="padding: 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.3); background: rgba(0,0,0,0.2); color: #fff; font-size: 1em;"
|
||
placeholder="${placeholder}">
|
||
<small style="color: #aaa; font-style: italic;">提示:输入内容将用于更新项目。</small>
|
||
</div>
|
||
</div>
|
||
<div class="popup-controls" style="display: flex; gap: 10px; justify-content: flex-end; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||
<div class="popup-button-cancel menu_button interactable" style="background: rgba(120,120,120,0.2); border-color: rgba(120,120,120,0.4);">
|
||
<i class="fas fa-times"></i> 取消
|
||
</div>
|
||
<div class="popup-button-ok menu_button menu_button_primary interactable" style="background: rgba(158,138,255,0.3); border-color: rgba(158,138,255,0.6);">
|
||
<i class="fas fa-check"></i> 确认
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</dialog>`;
|
||
|
||
const dialogElement = $(dialogHtml).appendTo('body');
|
||
const input = dialogElement.find('#generic-input');
|
||
|
||
const closeDialog = () => {
|
||
dialogElement[0].close();
|
||
dialogElement.remove();
|
||
};
|
||
|
||
const save = () => {
|
||
const newValue = input.val().trim();
|
||
if (newValue && newValue !== currentValue) {
|
||
onSave(newValue);
|
||
} else if (!newValue) {
|
||
toastr.warning('名称不能为空!');
|
||
input.focus();
|
||
return;
|
||
}
|
||
closeDialog();
|
||
};
|
||
|
||
dialogElement.find('.popup-button-ok').on('click', save);
|
||
dialogElement.find('.popup-button-cancel').on('click', closeDialog);
|
||
input.on('keypress', (e) => { if (e.which === 13) save(); });
|
||
input.on('keydown', (e) => { if (e.which === 27) closeDialog(); });
|
||
|
||
dialogElement[0].showModal();
|
||
input.focus().select();
|
||
}
|
||
|
||
|
||
function showColumnNameEditor(tableIndex, colIndex, currentName) {
|
||
showInputDialog({
|
||
title: '编辑列名',
|
||
label: '列名:',
|
||
currentValue: currentName,
|
||
placeholder: '请输入列名...',
|
||
onSave: (newName) => {
|
||
TableManager.updateHeader(tableIndex, colIndex, newName);
|
||
renderTables();
|
||
toastr.success(`列名已更新为 "${newName}"`);
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
function showTableNameEditor(tableIndex, currentName) {
|
||
showInputDialog({
|
||
title: '编辑表名',
|
||
label: '表名:',
|
||
currentValue: currentName,
|
||
placeholder: '请输入表名...',
|
||
onSave: (newName) => {
|
||
TableManager.renameTable(tableIndex, newName);
|
||
renderTables();
|
||
toastr.success(`表名已更新为 "${newName}"`);
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
function positionContextMenu(menu, trigger) {
|
||
menu.style.position = 'absolute';
|
||
menu.style.zIndex = '10000';
|
||
menu.style.left = '0';
|
||
menu.style.right = 'auto';
|
||
menu.style.marginTop = '';
|
||
menu.style.marginBottom = '';
|
||
menu.style.maxHeight = '';
|
||
menu.style.overflowY = '';
|
||
|
||
const viewportHeight = window.innerHeight;
|
||
const triggerRect = trigger.getBoundingClientRect();
|
||
const menuHeight = 200;
|
||
const scrollContainer = trigger.closest('.hly-scroll');
|
||
const containerRect = scrollContainer ? scrollContainer.getBoundingClientRect() : { top: 0, bottom: viewportHeight };
|
||
|
||
const spaceBelow = Math.min(viewportHeight, containerRect.bottom) - triggerRect.bottom;
|
||
const spaceAbove = triggerRect.top - Math.max(0, containerRect.top);
|
||
|
||
if (spaceBelow < menuHeight && spaceAbove > spaceBelow) {
|
||
menu.style.top = 'auto';
|
||
menu.style.bottom = '100%';
|
||
menu.style.marginBottom = '2px';
|
||
} else {
|
||
menu.style.top = '100%';
|
||
menu.style.bottom = 'auto';
|
||
menu.style.marginTop = '2px';
|
||
}
|
||
|
||
const menuWidth = 160;
|
||
const table = trigger.closest('table');
|
||
const tableWrapper = table ? table.closest('div[style*="overflowX"]') : null;
|
||
|
||
if (tableWrapper) {
|
||
const wrapperRect = tableWrapper.getBoundingClientRect();
|
||
const triggerLeftInWrapper = triggerRect.left - wrapperRect.left;
|
||
|
||
if (triggerLeftInWrapper + menuWidth > wrapperRect.width - 20) {
|
||
menu.style.left = 'auto';
|
||
menu.style.right = '0';
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
export function renderTables() {
|
||
let tables = TableManager.getMemoryState();
|
||
if (!tables) {
|
||
log('内存状态为空,从聊天记录加载作为后备。', 'warn');
|
||
tables = TableManager.loadTables();
|
||
}
|
||
|
||
const container = getAllTablesContainer();
|
||
|
||
if (!tables || !container) {
|
||
console.error('[内存储司-工部] 缺少表格数据或容器,无法渲染。');
|
||
return;
|
||
}
|
||
|
||
const highlights = TableManager.getHighlights();
|
||
|
||
const placeholder = document.getElementById('add-table-placeholder');
|
||
if (placeholder) {
|
||
placeholder.remove();
|
||
}
|
||
|
||
container.innerHTML = '';
|
||
|
||
tables.forEach((tableData, tableIndex) => {
|
||
const header = document.createElement('div');
|
||
header.style.display = 'flex';
|
||
header.style.justifyContent = 'space-between';
|
||
header.style.alignItems = 'center';
|
||
const title = document.createElement('h3');
|
||
title.innerHTML = `<i class="fas fa-table table-rename-icon" data-table-index="${tableIndex}" title="重命名"></i> ${tableData.name}`;
|
||
const controls = document.createElement('div');
|
||
controls.className = 'table-controls';
|
||
|
||
const moveUpBtn = tableIndex > 0 ? `<button class="menu_button small_button move-table-up-btn" data-table-index="${tableIndex}" title="上移"><i class="fas fa-arrow-up"></i></button>` : '';
|
||
const moveDownBtn = tableIndex < tables.length - 1 ? `<button class="menu_button small_button move-table-down-btn" data-table-index="${tableIndex}" title="下移"><i class="fas fa-arrow-down"></i></button>` : '';
|
||
|
||
controls.innerHTML = `
|
||
${moveUpBtn}
|
||
${moveDownBtn}
|
||
<button class="menu_button small_button edit-rules-btn" data-table-index="${tableIndex}" title="编辑规则"><i class="fa-solid fa-scroll"></i></button>
|
||
<button class="menu_button small_button danger delete-table-btn" data-table-index="${tableIndex}" title="废黜此表"><i class="fas fa-trash-alt"></i></button>
|
||
`;
|
||
header.appendChild(title);
|
||
header.appendChild(controls);
|
||
container.appendChild(header);
|
||
|
||
const tableElement = document.createElement('table');
|
||
tableElement.style.display = 'block';
|
||
tableElement.style.overflowX = 'auto';
|
||
tableElement.id = `amily2-table-${tableIndex}`;
|
||
tableElement.dataset.tableIndex = tableIndex;
|
||
|
||
const thead = tableElement.createTHead();
|
||
const headerRow = thead.insertRow();
|
||
|
||
const indexTh = document.createElement('th');
|
||
indexTh.className = 'index-col';
|
||
indexTh.textContent = '#';
|
||
headerRow.appendChild(indexTh);
|
||
|
||
tableData.headers.forEach((headerText, colIndex) => {
|
||
const th = document.createElement('th');
|
||
th.dataset.colIndex = colIndex;
|
||
th.style.cursor = 'pointer';
|
||
|
||
const headerContent = document.createElement('span');
|
||
headerContent.className = 'amily2-header-text';
|
||
headerContent.textContent = headerText;
|
||
th.appendChild(headerContent);
|
||
|
||
const menu = document.createElement('div');
|
||
menu.className = 'amily2-context-menu';
|
||
|
||
const actions = [
|
||
{ label: '向左移动', action: 'move-left', icon: 'fa-arrow-left' },
|
||
{ label: '向右移动', action: 'move-right', icon: 'fa-arrow-right' },
|
||
{ label: '在左加列', action: 'add-left', icon: 'fa-plus-circle' },
|
||
{ label: '在右加列', action: 'add-right', icon: 'fa-plus-circle' },
|
||
{ label: '编辑列名', action: 'rename', icon: 'fa-pen' },
|
||
{ label: '删除该列', action: 'delete', icon: 'fa-trash-alt', isDanger: true }
|
||
];
|
||
|
||
actions.forEach(({ label, action, icon, isDanger }) => {
|
||
const button = document.createElement('button');
|
||
button.textContent = label;
|
||
button.className = 'menu_button small_button';
|
||
if (isDanger) button.classList.add('danger');
|
||
|
||
button.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
switch (action) {
|
||
case 'move-left':
|
||
TableManager.moveColumn(tableIndex, colIndex, 'left');
|
||
break;
|
||
case 'move-right':
|
||
TableManager.moveColumn(tableIndex, colIndex, 'right');
|
||
break;
|
||
case 'add-left':
|
||
TableManager.insertColumn(tableIndex, colIndex, 'left');
|
||
break;
|
||
case 'add-right':
|
||
TableManager.insertColumn(tableIndex, colIndex, 'right');
|
||
break;
|
||
case 'rename':
|
||
showColumnNameEditor(tableIndex, colIndex, headerText);
|
||
break;
|
||
case 'delete':
|
||
if (confirm(`您确定要删除 “${headerText}” 列吗?`)) {
|
||
TableManager.deleteColumn(tableIndex, colIndex);
|
||
}
|
||
break;
|
||
}
|
||
renderTables();
|
||
});
|
||
menu.appendChild(button);
|
||
});
|
||
|
||
th.appendChild(menu);
|
||
headerRow.appendChild(th);
|
||
});
|
||
|
||
const tbody = tableElement.createTBody();
|
||
if (tableData.rows && tableData.rows.length > 0) {
|
||
tableData.rows.forEach((rowData, rowIndex) => {
|
||
const row = tbody.insertRow();
|
||
row.dataset.rowIndex = rowIndex;
|
||
|
||
const indexCell = row.insertCell();
|
||
indexCell.className = 'index-col';
|
||
|
||
const rowIndexSpan = document.createElement('span');
|
||
rowIndexSpan.textContent = rowIndex + 1;
|
||
indexCell.appendChild(rowIndexSpan);
|
||
|
||
const menu = document.createElement('div');
|
||
menu.className = 'amily2-context-menu amily2-row-context-menu';
|
||
|
||
const actions = [
|
||
{ label: '向上移动', action: 'move-up', icon: 'fa-arrow-up' },
|
||
{ label: '向下移动', action: 'move-down', icon: 'fa-arrow-down' },
|
||
{ label: '在上加行', action: 'add-above', icon: 'fa-plus-circle' },
|
||
{ label: '在下加行', action: 'add-below', icon: 'fa-plus-circle' },
|
||
{ label: '删除该行', action: 'delete-row', icon: 'fa-trash-alt', isDanger: true }
|
||
];
|
||
|
||
actions.forEach(({ label, action, icon, isDanger }) => {
|
||
const button = document.createElement('button');
|
||
button.innerHTML = `<i class="fas ${icon}"></i> ${label}`;
|
||
button.className = 'menu_button small_button';
|
||
if (isDanger) button.classList.add('danger');
|
||
|
||
button.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
|
||
switch (action) {
|
||
case 'move-up':
|
||
TableManager.moveRow(tableIndex, rowIndex, 'up');
|
||
break;
|
||
case 'move-down':
|
||
TableManager.moveRow(tableIndex, rowIndex, 'down');
|
||
break;
|
||
case 'add-above':
|
||
TableManager.insertRow(tableIndex, rowIndex, 'above');
|
||
break;
|
||
case 'add-below':
|
||
TableManager.insertRow(tableIndex, rowIndex, 'below');
|
||
break;
|
||
case 'delete-row':
|
||
if (confirm(`您确定要删除第 ${rowIndex + 1} 行吗?`)) {
|
||
TableManager.deleteRow(tableIndex, rowIndex);
|
||
}
|
||
break;
|
||
}
|
||
renderTables();
|
||
});
|
||
menu.appendChild(button);
|
||
});
|
||
indexCell.appendChild(menu);
|
||
|
||
rowData.forEach((cellData, colIndex) => {
|
||
const cell = row.insertCell();
|
||
cell.textContent = cellData;
|
||
|
||
const isTouchDevice = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||
if (!isTouchDevice()) {
|
||
cell.setAttribute('contenteditable', 'true');
|
||
}
|
||
cell.dataset.colIndex = colIndex;
|
||
cell.dataset.label = tableData.headers[colIndex] || '';
|
||
|
||
const highlightKey = `${tableIndex}-${rowIndex}-${colIndex}`;
|
||
if (highlights.has(highlightKey)) {
|
||
cell.classList.add('cell-highlight');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
container.appendChild(tableElement);
|
||
});
|
||
|
||
if (placeholder) {
|
||
container.appendChild(placeholder);
|
||
}
|
||
}
|
||
|
||
|
||
function openRuleEditor(tableIndex) {
|
||
const tables = TableManager.getMemoryState();
|
||
if (!tables || !tables[tableIndex]) return;
|
||
const table = tables[tableIndex];
|
||
|
||
const dialogHtml = `
|
||
<dialog class="popup wide_dialogue_popup large_dialogue_popup">
|
||
<div class="popup-body">
|
||
<h4 style="margin-top:0; color: #eee; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 10px;">
|
||
<i class="fa-solid fa-scroll"></i> 编辑 “${table.name}” 的规则
|
||
</h4>
|
||
<div class="popup-content" style="height: 70vh; overflow-y: auto;">
|
||
<div class="rule-editor-form" style="display: flex; flex-direction: column; gap: 15px; padding: 10px;">
|
||
<div class="rule-editor-field">
|
||
<label for="rule-note">【说明】:</label>
|
||
<textarea id="rule-note" class="text_pole" rows="5" style="width: 100%;">${table.note || ''}</textarea>
|
||
</div>
|
||
<div class="rule-editor-field">
|
||
<label for="rule-add">【增加】:</label>
|
||
<textarea id="rule-add" class="text_pole" rows="3" style="width: 100%;">${table.rule_add || ''}</textarea>
|
||
</div>
|
||
<div class="rule-editor-field">
|
||
<label for="rule-delete">【删除】:</label>
|
||
<textarea id="rule-delete" class="text_pole" rows="3" style="width: 100%;">${table.rule_delete || ''}</textarea>
|
||
</div>
|
||
<div class="rule-editor-field">
|
||
<label for="rule-update">【修改】:</label>
|
||
<textarea id="rule-update" class="text_pole" rows="3" style="width: 100%;">${table.rule_update || ''}</textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="popup-controls">
|
||
<div class="popup-button-ok menu_button menu_button_primary interactable">保存</div>
|
||
<div class="popup-button-cancel menu_button interactable" style="margin-left: 10px;">取消</div>
|
||
</div>
|
||
</div>
|
||
</dialog>`;
|
||
|
||
const dialogElement = $(dialogHtml).appendTo('body');
|
||
|
||
const closeDialog = () => {
|
||
dialogElement[0].close();
|
||
dialogElement.remove();
|
||
};
|
||
|
||
dialogElement.find('.popup-button-ok').on('click', () => {
|
||
const newRules = {
|
||
note: dialogElement.find('#rule-note').val(),
|
||
rule_add: dialogElement.find('#rule-add').val(),
|
||
rule_delete: dialogElement.find('#rule-delete').val(),
|
||
rule_update: dialogElement.find('#rule-update').val(),
|
||
};
|
||
TableManager.updateTableRules(tableIndex, newRules);
|
||
closeDialog();
|
||
});
|
||
|
||
dialogElement.find('.popup-button-cancel').on('click', closeDialog);
|
||
dialogElement[0].showModal();
|
||
}
|
||
|
||
|
||
function bindInjectionSettings() {
|
||
const settings = extension_settings[extensionName];
|
||
|
||
const masterSwitchCheckbox = document.getElementById('table-system-master-switch');
|
||
const enabledCheckbox = document.getElementById('table-injection-enabled');
|
||
const positionSelect = document.getElementById('table-injection-position');
|
||
const depthInput = document.getElementById('table-injection-depth');
|
||
const roleRadioGroup = document.querySelectorAll('input[name="table-injection-role"]');
|
||
|
||
if (!masterSwitchCheckbox || !enabledCheckbox || !positionSelect || !depthInput || !roleRadioGroup.length) {
|
||
return;
|
||
}
|
||
|
||
const updateInjectionUI = () => {
|
||
const position = positionSelect.value;
|
||
const masterEnabled = masterSwitchCheckbox.checked;
|
||
|
||
const isChatInjection = position === '1';
|
||
|
||
enabledCheckbox.disabled = !masterEnabled;
|
||
positionSelect.disabled = !masterEnabled;
|
||
depthInput.disabled = !masterEnabled || !isChatInjection;
|
||
roleRadioGroup.forEach(radio => radio.disabled = !masterEnabled || !isChatInjection);
|
||
|
||
const enabledOpacity = masterEnabled ? '1' : '0.5';
|
||
enabledCheckbox.style.opacity = enabledOpacity;
|
||
if (enabledCheckbox.closest('.control-block-with-switch')) {
|
||
enabledCheckbox.closest('.control-block-with-switch').style.opacity = enabledOpacity;
|
||
}
|
||
|
||
positionSelect.style.opacity = enabledOpacity;
|
||
if (positionSelect.previousElementSibling) {
|
||
positionSelect.previousElementSibling.style.opacity = enabledOpacity;
|
||
}
|
||
|
||
const depthOpacity = masterEnabled && isChatInjection ? '1' : '0.5';
|
||
depthInput.style.opacity = depthOpacity;
|
||
if (depthInput.previousElementSibling) {
|
||
depthInput.previousElementSibling.style.opacity = depthOpacity;
|
||
}
|
||
|
||
const roleOpacity = masterEnabled && isChatInjection ? '1' : '0.5';
|
||
const roleGroupContainer = document.getElementById('table-role-system')?.closest('.radio-group');
|
||
if (roleGroupContainer) {
|
||
roleGroupContainer.style.opacity = roleOpacity;
|
||
if (roleGroupContainer.previousElementSibling) {
|
||
roleGroupContainer.previousElementSibling.style.opacity = roleOpacity;
|
||
}
|
||
}
|
||
|
||
const fillingModeRadios = document.querySelectorAll('input[name="filling-mode"]');
|
||
fillingModeRadios.forEach(radio => {
|
||
radio.disabled = !masterEnabled;
|
||
const label = radio.closest('label');
|
||
if (label) {
|
||
label.style.opacity = masterEnabled ? '1' : '0.5';
|
||
}
|
||
});
|
||
|
||
const fillButton = document.getElementById('fill-table-now-btn');
|
||
if (fillButton) {
|
||
fillButton.disabled = !masterEnabled;
|
||
fillButton.style.opacity = masterEnabled ? '1' : '0.5';
|
||
}
|
||
};
|
||
|
||
masterSwitchCheckbox.checked = settings.table_system_enabled !== false;
|
||
enabledCheckbox.checked = settings.table_injection_enabled;
|
||
positionSelect.value = settings.injection.position;
|
||
depthInput.value = settings.injection.depth;
|
||
roleRadioGroup.forEach(radio => {
|
||
if (parseInt(radio.value, 10) === settings.injection.role) {
|
||
radio.checked = true;
|
||
}
|
||
});
|
||
|
||
updateInjectionUI();
|
||
|
||
masterSwitchCheckbox.addEventListener('change', () => {
|
||
settings.table_system_enabled = masterSwitchCheckbox.checked;
|
||
saveSettingsDebounced();
|
||
updateInjectionUI();
|
||
|
||
const statusText = masterSwitchCheckbox.checked ? '已启用' : '已禁用';
|
||
toastr.info(`表格系统总开关${statusText}。`);
|
||
log(`表格系统总开关${statusText}。`, 'info');
|
||
});
|
||
|
||
enabledCheckbox.addEventListener('change', () => {
|
||
settings.table_injection_enabled = enabledCheckbox.checked;
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
positionSelect.addEventListener('change', () => {
|
||
settings.injection.position = parseInt(positionSelect.value, 10);
|
||
saveSettingsDebounced();
|
||
|
||
updateInjectionUI();
|
||
});
|
||
|
||
depthInput.addEventListener('input', () => {
|
||
settings.injection.depth = parseInt(depthInput.value, 10);
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
roleRadioGroup.forEach(radio => {
|
||
radio.addEventListener('change', () => {
|
||
if (radio.checked) {
|
||
settings.injection.role = parseInt(radio.value, 10);
|
||
saveSettingsDebounced();
|
||
}
|
||
});
|
||
});
|
||
|
||
log('表格注入设置已成功绑定。', 'success');
|
||
}
|
||
|
||
|
||
function updateAndSaveTableSetting(key, value) {
|
||
if (!extension_settings[extensionName]) {
|
||
extension_settings[extensionName] = {};
|
||
}
|
||
extension_settings[extensionName][key] = value;
|
||
saveSettingsDebounced();
|
||
}
|
||
|
||
function bindWorldBookSettings() {
|
||
const settings = extension_settings[extensionName];
|
||
|
||
if (settings.table_worldbook_enabled === undefined) settings.table_worldbook_enabled = false;
|
||
if (settings.table_worldbook_char_limit === undefined) settings.table_worldbook_char_limit = 30000;
|
||
if (settings.table_worldbook_source === undefined) settings.table_worldbook_source = 'character';
|
||
if (settings.table_selected_worldbooks === undefined) settings.table_selected_worldbooks = [];
|
||
if (settings.table_selected_entries === undefined) settings.table_selected_entries = {};
|
||
|
||
const enabledCheckbox = document.getElementById('table_worldbook_enabled');
|
||
const limitSlider = document.getElementById('table_worldbook_char_limit');
|
||
const limitValueSpan = document.getElementById('table_worldbook_char_limit_value');
|
||
const sourceRadios = document.querySelectorAll('input[name="table_worldbook_source"]');
|
||
const manualSelectWrapper = document.getElementById('table_worldbook_select_wrapper');
|
||
const refreshButton = document.getElementById('table_refresh_worldbooks');
|
||
const bookListContainer = document.getElementById('table_worldbook_checkbox_list');
|
||
const entryListContainer = document.getElementById('table_worldbook_entry_list');
|
||
|
||
if (!enabledCheckbox || !limitSlider || !limitValueSpan || !sourceRadios.length || !manualSelectWrapper || !refreshButton || !bookListContainer || !entryListContainer) {
|
||
log('无法找到世界书设置的相关UI元素,绑定失败。', 'warn');
|
||
return;
|
||
}
|
||
|
||
const saveSelectedEntries = () => {
|
||
const selected = {};
|
||
entryListContainer.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => {
|
||
const book = cb.dataset.book;
|
||
const uid = cb.dataset.uid;
|
||
if (!selected[book]) {
|
||
selected[book] = [];
|
||
}
|
||
selected[book].push(uid);
|
||
});
|
||
settings.table_selected_entries = selected;
|
||
saveSettingsDebounced();
|
||
};
|
||
|
||
const renderWorldBookEntries = async () => {
|
||
entryListContainer.innerHTML = '<p>加载条目中...</p>';
|
||
const source = settings.table_worldbook_source || 'character';
|
||
let bookNames = [];
|
||
|
||
if (source === 'manual') {
|
||
bookNames = settings.table_selected_worldbooks || [];
|
||
} else {
|
||
if (this_chid !== undefined && this_chid >= 0 && characters[this_chid]) {
|
||
try {
|
||
const charLorebooks = await safeCharLorebooks({ type: 'all' });
|
||
if (charLorebooks.primary) bookNames.push(charLorebooks.primary);
|
||
if (charLorebooks.additional?.length) bookNames.push(...charLorebooks.additional);
|
||
} catch (error) {
|
||
console.error(`[内存储司] 获取角色世界书失败:`, error);
|
||
entryListContainer.innerHTML = '<p class="notes" style="color:red;">获取角色世界书失败。</p>';
|
||
return;
|
||
}
|
||
} else {
|
||
entryListContainer.innerHTML = '<p class="notes">请先加载一个角色。</p>';
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (bookNames.length === 0) {
|
||
entryListContainer.innerHTML = '<p class="notes">未选择或绑定世界书。</p>';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const allEntries = [];
|
||
for (const bookName of bookNames) {
|
||
const entries = await safeLorebookEntries(bookName);
|
||
entries.forEach(entry => allEntries.push({ ...entry, bookName }));
|
||
}
|
||
|
||
entryListContainer.innerHTML = '';
|
||
if (allEntries.length === 0) {
|
||
entryListContainer.innerHTML = '<p class="notes">所选世界书中没有条目。</p>';
|
||
return;
|
||
}
|
||
|
||
allEntries.forEach(entry => {
|
||
const div = document.createElement('div');
|
||
div.className = 'checkbox-item';
|
||
div.title = `世界书: ${entry.bookName}\nUID: ${entry.uid}`;
|
||
|
||
const checkbox = document.createElement('input');
|
||
checkbox.type = 'checkbox';
|
||
checkbox.id = `wb-entry-check-${entry.bookName}-${entry.uid}`;
|
||
checkbox.dataset.book = entry.bookName;
|
||
checkbox.dataset.uid = entry.uid;
|
||
|
||
const isChecked = settings.table_selected_entries[entry.bookName]?.includes(String(entry.uid));
|
||
checkbox.checked = !!isChecked;
|
||
|
||
const label = document.createElement('label');
|
||
label.htmlFor = checkbox.id;
|
||
label.textContent = entry.comment || '无标题条目';
|
||
|
||
div.appendChild(checkbox);
|
||
div.appendChild(label);
|
||
entryListContainer.appendChild(div);
|
||
});
|
||
} catch (error) {
|
||
console.error(`[内存储司] 加载世界书条目失败:`, error);
|
||
entryListContainer.innerHTML = '<p class="notes" style="color:red;">加载条目失败。</p>';
|
||
}
|
||
};
|
||
|
||
const renderWorldBookList = () => {
|
||
const worldBooks = world_names.map(name => ({ name: name.replace('.json', ''), file_name: name }));
|
||
bookListContainer.innerHTML = '';
|
||
if (worldBooks && worldBooks.length > 0) {
|
||
worldBooks.forEach(book => {
|
||
const div = document.createElement('div');
|
||
div.className = 'checkbox-item';
|
||
div.title = book.name;
|
||
|
||
const checkbox = document.createElement('input');
|
||
checkbox.type = 'checkbox';
|
||
checkbox.id = `wb-check-${book.file_name}`;
|
||
checkbox.value = book.file_name;
|
||
checkbox.checked = settings.table_selected_worldbooks.includes(book.file_name);
|
||
|
||
checkbox.addEventListener('change', () => {
|
||
if (checkbox.checked) {
|
||
if (!settings.table_selected_worldbooks.includes(book.file_name)) {
|
||
settings.table_selected_worldbooks.push(book.file_name);
|
||
}
|
||
} else {
|
||
settings.table_selected_worldbooks = settings.table_selected_worldbooks.filter(name => name !== book.file_name);
|
||
}
|
||
saveSettingsDebounced();
|
||
renderWorldBookEntries();
|
||
});
|
||
|
||
const label = document.createElement('label');
|
||
label.htmlFor = `wb-check-${book.file_name}`;
|
||
label.textContent = book.name;
|
||
|
||
div.appendChild(checkbox);
|
||
div.appendChild(label);
|
||
bookListContainer.appendChild(div);
|
||
});
|
||
} else {
|
||
bookListContainer.innerHTML = '<p class="notes">没有找到世界书。</p>';
|
||
}
|
||
renderWorldBookEntries();
|
||
};
|
||
|
||
const updateManualSelectVisibility = () => {
|
||
const isManual = settings.table_worldbook_source === 'manual';
|
||
manualSelectWrapper.style.display = isManual ? 'block' : 'none';
|
||
renderWorldBookEntries();
|
||
if (isManual) {
|
||
renderWorldBookList();
|
||
}
|
||
};
|
||
|
||
enabledCheckbox.checked = settings.table_worldbook_enabled;
|
||
limitSlider.value = settings.table_worldbook_char_limit;
|
||
limitValueSpan.textContent = settings.table_worldbook_char_limit;
|
||
sourceRadios.forEach(radio => {
|
||
radio.checked = radio.value === settings.table_worldbook_source;
|
||
});
|
||
|
||
updateManualSelectVisibility();
|
||
|
||
enabledCheckbox.addEventListener('change', () => {
|
||
settings.table_worldbook_enabled = enabledCheckbox.checked;
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
limitSlider.addEventListener('input', () => { limitValueSpan.textContent = limitSlider.value; });
|
||
limitSlider.addEventListener('change', () => {
|
||
settings.table_worldbook_char_limit = parseInt(limitSlider.value, 10);
|
||
saveSettingsDebounced();
|
||
});
|
||
|
||
sourceRadios.forEach(radio => {
|
||
radio.addEventListener('change', () => {
|
||
if (radio.checked) {
|
||
settings.table_worldbook_source = radio.value;
|
||
updateManualSelectVisibility();
|
||
saveSettingsDebounced();
|
||
}
|
||
});
|
||
});
|
||
|
||
refreshButton.addEventListener('click', renderWorldBookList);
|
||
entryListContainer.addEventListener('change', (event) => {
|
||
if (event.target.type === 'checkbox') {
|
||
saveSelectedEntries();
|
||
}
|
||
});
|
||
|
||
log('世界书设置已成功绑定。', 'success');
|
||
}
|
||
|
||
export function bindTableEvents() {
|
||
const panel = document.getElementById('amily2_memorisation_forms_panel');
|
||
if (!panel || panel.dataset.eventsBound) {
|
||
return;
|
||
}
|
||
log('开始为表格视图绑定交互事件...', 'info');
|
||
const fillingModeRadios = panel.querySelectorAll('input[name="filling-mode"]');
|
||
const contextSliderContainer = document.getElementById('context-reading-slider-container');
|
||
const contextSlider = document.getElementById('context-reading-slider');
|
||
const contextValueSpan = document.getElementById('context-reading-value');
|
||
|
||
const updateFillingModeUI = () => {
|
||
const currentMode = extension_settings[extensionName]?.filling_mode || 'main-api';
|
||
fillingModeRadios.forEach(radio => {
|
||
radio.checked = (radio.value === currentMode);
|
||
});
|
||
|
||
if (contextSliderContainer) {
|
||
if (currentMode === 'secondary-api') {
|
||
contextSliderContainer.style.display = 'block';
|
||
} else {
|
||
contextSliderContainer.style.display = 'none';
|
||
}
|
||
}
|
||
};
|
||
|
||
fillingModeRadios.forEach(radio => {
|
||
radio.addEventListener('change', function() {
|
||
const selectedMode = this.value;
|
||
updateAndSaveTableSetting('filling_mode', selectedMode);
|
||
|
||
let modeName = '原始填表';
|
||
if (selectedMode === 'secondary-api') modeName = '分步填表';
|
||
if (selectedMode === 'optimized') modeName = '优化中填表';
|
||
|
||
toastr.info(`填表模式已切换为 ${modeName}。`);
|
||
updateFillingModeUI(); // 更新UI以确保状态同步
|
||
});
|
||
});
|
||
|
||
if (contextSlider && contextValueSpan) {
|
||
const contextReadingValue = extension_settings[extensionName]?.context_reading_level || 4;
|
||
contextSlider.value = contextReadingValue;
|
||
contextValueSpan.textContent = contextReadingValue;
|
||
|
||
contextSlider.addEventListener('input', function() {
|
||
contextValueSpan.textContent = this.value;
|
||
});
|
||
|
||
contextSlider.addEventListener('change', function() {
|
||
updateAndSaveTableSetting('context_reading_level', parseInt(this.value, 10));
|
||
toastr.info(`上下文读取级别已设置为 ${this.value}。`);
|
||
});
|
||
}
|
||
|
||
updateFillingModeUI();
|
||
|
||
const renderAll = () => {
|
||
renderTables();
|
||
bindInjectionSettings();
|
||
};
|
||
|
||
renderAll();
|
||
bindWorldBookSettings();
|
||
bindBatchFillButton(); // 【V36.0】绑定批量填表按钮
|
||
bindFloorFillButtons(); // 【新增】绑定楼层填表按钮
|
||
bindReorganizeButton(); // 【新增】绑定重新整理按钮
|
||
bindTemplateEditors(); // 【V55.1】为新的指令模板编辑器绑定事件
|
||
bindNccsApiEvents(); // 【新增】绑定Nccs API系统事件
|
||
|
||
const navDeck = document.querySelector('#amily2_memorisation_forms_panel .sinan-navigation-deck');
|
||
if (navDeck) {
|
||
navDeck.addEventListener('click', (event) => {
|
||
const target = event.target.closest('.sinan-nav-item');
|
||
if (!target) return;
|
||
|
||
const tabName = target.dataset.tab;
|
||
if (!tabName) return;
|
||
|
||
const container = target.closest('.settings-group');
|
||
if (!container) return;
|
||
|
||
container.querySelectorAll('.sinan-nav-item').forEach(btn => btn.classList.remove('active'));
|
||
target.classList.add('active');
|
||
container.querySelectorAll('.sinan-tab-pane').forEach(pane => pane.classList.remove('active'));
|
||
const activePane = container.querySelector(`#sinan-${tabName}-tab`);
|
||
if (activePane) {
|
||
activePane.classList.add('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
const exportBtn = document.getElementById('amily2-export-preset-btn');
|
||
const exportFullBtn = document.getElementById('amily2-export-preset-full-btn');
|
||
const importBtn = document.getElementById('amily2-import-preset-btn');
|
||
const importGlobalBtn = document.getElementById('amily2-import-global-preset-btn');
|
||
const clearGlobalBtn = document.getElementById('amily2-clear-global-preset-btn');
|
||
|
||
if (exportBtn) {
|
||
exportBtn.addEventListener('click', () => TableManager.exportPreset());
|
||
}
|
||
if (exportFullBtn) {
|
||
exportFullBtn.addEventListener('click', () => TableManager.exportPresetFull());
|
||
}
|
||
if (importBtn) {
|
||
importBtn.addEventListener('click', () => TableManager.importPreset(renderAll));
|
||
}
|
||
if (importGlobalBtn) {
|
||
importGlobalBtn.addEventListener('click', () => {
|
||
|
||
const isEmpty = TableManager.isCurrentTablesEmpty();
|
||
TableManager.importGlobalPreset(() => {
|
||
if (isEmpty) {
|
||
TableManager.loadTables();
|
||
renderAll();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
if (clearGlobalBtn) {
|
||
clearGlobalBtn.addEventListener('click', () => {
|
||
const isEmpty = TableManager.isCurrentTablesEmpty();
|
||
TableManager.clearGlobalPreset();
|
||
if (isEmpty) {
|
||
TableManager.loadTables();
|
||
renderAll();
|
||
}
|
||
});
|
||
}
|
||
|
||
const clearAllBtn = document.getElementById('amily2-clear-all-tables-btn');
|
||
if (clearAllBtn) {
|
||
clearAllBtn.addEventListener('click', () => {
|
||
if (confirm('【确认】您确定要清空所有表格的剧情内容吗?此操作将保留表格结构,但会删除所有已填写的行。')) {
|
||
TableManager.clearAllTables();
|
||
renderAll();
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
const addTablePlaceholder = document.getElementById('add-table-placeholder');
|
||
if (addTablePlaceholder) {
|
||
addTablePlaceholder.addEventListener('click', () => {
|
||
const newName = prompt('请输入新表格的名称:', '新表格');
|
||
if (newName && newName.trim()) {
|
||
TableManager.addTable(newName.trim());
|
||
renderAll();
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
const allTablesContainer = getAllTablesContainer();
|
||
if (allTablesContainer) {
|
||
allTablesContainer.addEventListener('click', (event) => {
|
||
const th = event.target.closest('th');
|
||
if (th && !th.classList.contains('index-col')) {
|
||
toggleColumnContextMenu(event);
|
||
return;
|
||
}
|
||
|
||
const td = event.target.closest('td.index-col');
|
||
if (td) {
|
||
toggleRowContextMenu(event);
|
||
return;
|
||
}
|
||
|
||
const renameIcon = event.target.closest('.table-rename-icon');
|
||
if (renameIcon) {
|
||
const tableIndex = parseInt(renameIcon.dataset.tableIndex, 10);
|
||
const tables = TableManager.getMemoryState();
|
||
const currentName = tables[tableIndex]?.name || '';
|
||
showTableNameEditor(tableIndex, currentName);
|
||
return;
|
||
}
|
||
|
||
const target = event.target.closest('button');
|
||
if (!target) return;
|
||
|
||
const tableIndex = parseInt(target.dataset.tableIndex, 10);
|
||
|
||
if (target.matches('.add-row-btn')) {
|
||
TableManager.addRow(tableIndex);
|
||
renderAll();
|
||
} else if (target.matches('.add-col-btn')) {
|
||
TableManager.addColumn(tableIndex);
|
||
renderAll();
|
||
} else if (target.matches('.move-table-up-btn') || target.matches('.move-table-down-btn')) {
|
||
const direction = target.classList.contains('move-table-up-btn') ? 'up' : 'down';
|
||
TableManager.moveTable(tableIndex, direction);
|
||
renderAll();
|
||
} else if (target.matches('.edit-rules-btn')) {
|
||
openRuleEditor(tableIndex);
|
||
} else if (target.matches('.delete-table-btn')) {
|
||
const tables = TableManager.getMemoryState();
|
||
const tableName = tables[tableIndex]?.name || '未知表格';
|
||
if (confirm(`【最终警告】您确定要永久废黜表格 “[${tableName}]” 吗?此操作不可逆!`)) {
|
||
TableManager.deleteTable(tableIndex);
|
||
renderAll();
|
||
}
|
||
}
|
||
});
|
||
|
||
const isTouchDevice = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||
if (isTouchDevice()) {
|
||
let lastTap = 0;
|
||
let lastTapTarget = null;
|
||
allTablesContainer.addEventListener('touchstart', (event) => {
|
||
const target = event.target.closest('td');
|
||
if (!target || target.dataset.colIndex === undefined) return;
|
||
|
||
const currentTime = new Date().getTime();
|
||
const tapLength = currentTime - lastTap;
|
||
if (tapLength < 300 && tapLength > 0 && lastTapTarget === target) {
|
||
event.preventDefault();
|
||
if (target.getAttribute('contenteditable') !== 'true') {
|
||
target.setAttribute('contenteditable', 'true');
|
||
setTimeout(() => target.focus(), 0);
|
||
}
|
||
}
|
||
lastTap = currentTime;
|
||
lastTapTarget = target;
|
||
});
|
||
}
|
||
|
||
allTablesContainer.addEventListener('blur', (event) => {
|
||
const target = event.target;
|
||
if (target.tagName !== 'TD' || target.getAttribute('contenteditable') !== 'true') return;
|
||
|
||
const isTouchDevice = () => 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||
if (isTouchDevice()) {
|
||
target.setAttribute('contenteditable', 'false');
|
||
}
|
||
|
||
const tableElement = target.closest('table');
|
||
if (!tableElement) return;
|
||
|
||
const tableIndex = parseInt(tableElement.dataset.tableIndex, 10);
|
||
const rowIndex = parseInt(target.closest('tr').dataset.rowIndex, 10);
|
||
const colIndex = parseInt(target.dataset.colIndex, 10);
|
||
const newValue = target.textContent;
|
||
|
||
TableManager.addHighlight(tableIndex, rowIndex, colIndex);
|
||
const dataToUpdate = { [colIndex]: newValue };
|
||
TableManager.updateRow(tableIndex, rowIndex, dataToUpdate);
|
||
|
||
renderAll();
|
||
|
||
}, true); // Use capture phase
|
||
}
|
||
|
||
panel.dataset.eventsBound = 'true';
|
||
log('表格视图交互事件已成功绑定。', 'success');
|
||
|
||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||
console.log(`[${extensionName}] 检测到角色/聊天切换,正在刷新表格系统UI和世界书设置...`);
|
||
renderAll();
|
||
|
||
setTimeout(() => {
|
||
const settings = extension_settings[extensionName];
|
||
if (settings && settings.table_worldbook_enabled) {
|
||
try {
|
||
bindWorldBookSettings();
|
||
console.log(`[${extensionName}] 世界书设置已刷新`);
|
||
} catch (error) {
|
||
console.error(`[${extensionName}] 刷新世界书设置时出错:`, error);
|
||
}
|
||
}
|
||
}, 100);
|
||
});
|
||
}
|
||
|
||
function bindBatchFillButton() {
|
||
const fillButton = document.getElementById('fill-table-now-btn');
|
||
if (fillButton) {
|
||
if (fillButton.dataset.batchEventBound) return;
|
||
|
||
fillButton.addEventListener('click', (event) => {
|
||
const settings = extension_settings[extensionName];
|
||
const tableSystemEnabled = settings.table_system_enabled !== false;
|
||
|
||
if (!tableSystemEnabled) {
|
||
event.preventDefault();
|
||
toastr.warning('表格系统总开关已关闭,请先启用总开关。');
|
||
return;
|
||
}
|
||
|
||
startBatchFilling();
|
||
});
|
||
|
||
fillButton.dataset.batchEventBound = 'true';
|
||
log('"立即填表"按钮已成功绑定。', 'success');
|
||
}
|
||
}
|
||
|
||
function bindReorganizeButton() {
|
||
const reorganizeBtn = document.getElementById('reorganize-table-btn');
|
||
|
||
if (reorganizeBtn) {
|
||
if (reorganizeBtn.dataset.reorganizeEventBound) return;
|
||
|
||
reorganizeBtn.addEventListener('click', async (event) => {
|
||
const settings = extension_settings[extensionName];
|
||
const tableSystemEnabled = settings.table_system_enabled !== false;
|
||
|
||
if (!tableSystemEnabled) {
|
||
event.preventDefault();
|
||
toastr.warning('表格系统总开关已关闭,请先启用总开关。');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { reorganizeTableContent } = await import('../core/table-system/reorganizer.js');
|
||
await reorganizeTableContent();
|
||
} catch (error) {
|
||
console.error('[内存储司] 重新整理功能导入失败:', error);
|
||
toastr.error('重新整理功能启动失败,请检查系统状态。');
|
||
}
|
||
});
|
||
|
||
reorganizeBtn.dataset.reorganizeEventBound = 'true';
|
||
log('"重新整理"按钮已成功绑定。', 'success');
|
||
}
|
||
}
|
||
|
||
|
||
function bindFloorFillButtons() {
|
||
const selectedFloorsBtn = document.getElementById('fill-selected-floors-btn');
|
||
const currentFloorBtn = document.getElementById('fill-current-floor-btn');
|
||
|
||
if (selectedFloorsBtn) {
|
||
|
||
if (selectedFloorsBtn.dataset.floorEventBound) return;
|
||
|
||
selectedFloorsBtn.addEventListener('click', (event) => {
|
||
const settings = extension_settings[extensionName];
|
||
const tableSystemEnabled = settings.table_system_enabled !== false;
|
||
|
||
if (!tableSystemEnabled) {
|
||
event.preventDefault();
|
||
toastr.warning('表格系统总开关已关闭,请先启用总开关。');
|
||
return;
|
||
}
|
||
|
||
const startFloorInput = document.getElementById('floor-start-input');
|
||
const endFloorInput = document.getElementById('floor-end-input');
|
||
|
||
const startFloor = parseInt(startFloorInput.value, 10);
|
||
const endFloor = parseInt(endFloorInput.value, 10);
|
||
|
||
if (!startFloor || !endFloor) {
|
||
toastr.warning('请输入有效的起始楼层和结束楼层。');
|
||
return;
|
||
}
|
||
|
||
if (startFloor > endFloor) {
|
||
toastr.warning('起始楼层不能大于结束楼层。');
|
||
return;
|
||
}
|
||
|
||
if (startFloor < 1) {
|
||
toastr.warning('楼层不能小于1。');
|
||
return;
|
||
}
|
||
|
||
import('../core/table-system/batch-filler.js').then(module => {
|
||
module.startFloorRangeFilling(startFloor, endFloor);
|
||
});
|
||
});
|
||
|
||
selectedFloorsBtn.dataset.floorEventBound = 'true';
|
||
log('"选定楼层填表"按钮已成功绑定。', 'success');
|
||
}
|
||
|
||
if (currentFloorBtn) {
|
||
if (currentFloorBtn.dataset.currentEventBound) return;
|
||
|
||
currentFloorBtn.addEventListener('click', (event) => {
|
||
const settings = extension_settings[extensionName];
|
||
const tableSystemEnabled = settings.table_system_enabled !== false;
|
||
|
||
if (!tableSystemEnabled) {
|
||
event.preventDefault();
|
||
toastr.warning('表格系统总开关已关闭,请先启用总开关。');
|
||
return;
|
||
}
|
||
|
||
import('../core/table-system/batch-filler.js').then(module => {
|
||
module.startCurrentFloorFilling();
|
||
});
|
||
});
|
||
|
||
currentFloorBtn.dataset.currentEventBound = 'true';
|
||
log('"填当前楼层"按钮已成功绑定。', 'success');
|
||
}
|
||
}
|
||
|
||
function bindTemplateEditors() {
|
||
const ruleEditor = document.getElementById('ai-rule-template-editor');
|
||
const ruleSaveBtn = document.getElementById('ai-rule-template-save-btn');
|
||
const ruleRestoreBtn = document.getElementById('ai-rule-template-restore-btn');
|
||
|
||
const flowEditor = document.getElementById('ai-flow-template-editor');
|
||
const flowSaveBtn = document.getElementById('ai-flow-template-save-btn');
|
||
const flowRestoreBtn = document.getElementById('ai-flow-template-restore-btn');
|
||
|
||
if (!ruleEditor || !flowEditor) {
|
||
log('无法找到指令模板编辑器,绑定失败。', 'warn');
|
||
return;
|
||
}
|
||
|
||
ruleEditor.value = TableManager.getBatchFillerRuleTemplate();
|
||
flowEditor.value = TableManager.getBatchFillerFlowTemplate();
|
||
|
||
ruleSaveBtn.addEventListener('click', () => {
|
||
TableManager.saveBatchFillerRuleTemplate(ruleEditor.value);
|
||
toastr.success('规则提示词已保存。');
|
||
log('批量填表-规则提示词已保存。', 'success');
|
||
});
|
||
|
||
flowSaveBtn.addEventListener('click', () => {
|
||
TableManager.saveBatchFillerFlowTemplate(flowEditor.value);
|
||
toastr.success('流程提示词已保存。');
|
||
log('批量填表-流程提示词已保存。', 'success');
|
||
});
|
||
|
||
ruleRestoreBtn.addEventListener('click', () => {
|
||
if (confirm('您确定要将规则提示词恢复为默认设置吗?')) {
|
||
ruleEditor.value = DEFAULT_AI_RULE_TEMPLATE;
|
||
TableManager.saveBatchFillerRuleTemplate(ruleEditor.value);
|
||
toastr.info('规则提示词已恢复为默认。');
|
||
log('批量填表-规则提示词已恢复默认。', 'info');
|
||
}
|
||
});
|
||
|
||
flowRestoreBtn.addEventListener('click', () => {
|
||
if (confirm('您确定要将流程提示词恢复为默认设置吗?')) {
|
||
flowEditor.value = DEFAULT_AI_FLOW_TEMPLATE;
|
||
TableManager.saveBatchFillerFlowTemplate(flowEditor.value);
|
||
toastr.info('流程提示词已恢复为默认。');
|
||
log('批量填表-流程提示词已恢复默认。', 'info');
|
||
}
|
||
});
|
||
|
||
log('指令模板编辑器已成功绑定。', 'success');
|
||
}
|
||
|
||
function bindNccsApiEvents() {
|
||
const settings = extension_settings[extensionName];
|
||
|
||
if (settings.nccsEnabled === undefined) settings.nccsEnabled = false;
|
||
if (settings.nccsApiMode === undefined) settings.nccsApiMode = 'openai_test';
|
||
if (settings.nccsApiUrl === undefined) settings.nccsApiUrl = 'https://api.openai.com/v1';
|
||
if (settings.nccsApiKey === undefined) settings.nccsApiKey = '';
|
||
if (settings.nccsModel === undefined) settings.nccsModel = '';
|
||
if (settings.nccsMaxTokens === undefined) settings.nccsMaxTokens = 2000;
|
||
if (settings.nccsTemperature === undefined) settings.nccsTemperature = 0.7;
|
||
if (settings.nccsTavernProfile === undefined) settings.nccsTavernProfile = '';
|
||
|
||
const enabledToggle = document.getElementById('nccs-api-enabled');
|
||
const configDiv = document.getElementById('nccs-api-config');
|
||
const modeSelect = document.getElementById('nccs-api-mode');
|
||
const urlInput = document.getElementById('nccs-api-url');
|
||
const keyInput = document.getElementById('nccs-api-key');
|
||
const modelInput = document.getElementById('nccs-api-model');
|
||
const maxTokensSlider = document.getElementById('nccs-max-tokens');
|
||
const maxTokensValue = document.getElementById('nccs-max-tokens-value');
|
||
const temperatureSlider = document.getElementById('nccs-temperature');
|
||
const temperatureValue = document.getElementById('nccs-temperature-value');
|
||
const presetSelect = document.getElementById('nccs-sillytavern-preset');
|
||
const testButton = document.getElementById('nccs-test-connection');
|
||
const fetchModelsButton = document.getElementById('nccs-fetch-models');
|
||
|
||
if (!enabledToggle || !configDiv) return;
|
||
|
||
enabledToggle.checked = settings.nccsEnabled;
|
||
if (modeSelect) modeSelect.value = settings.nccsApiMode;
|
||
if (urlInput) urlInput.value = settings.nccsApiUrl;
|
||
if (keyInput) keyInput.value = settings.nccsApiKey;
|
||
if (modelInput) modelInput.value = settings.nccsModel;
|
||
if (maxTokensSlider) {
|
||
maxTokensSlider.value = settings.nccsMaxTokens;
|
||
if (maxTokensValue) maxTokensValue.textContent = settings.nccsMaxTokens;
|
||
}
|
||
if (temperatureSlider) {
|
||
temperatureSlider.value = settings.nccsTemperature;
|
||
if (temperatureValue) temperatureValue.textContent = settings.nccsTemperature;
|
||
}
|
||
if (presetSelect) presetSelect.value = settings.nccsTavernProfile || '';
|
||
|
||
const updateConfigVisibility = () => {
|
||
configDiv.style.display = enabledToggle.checked ? 'block' : 'none';
|
||
};
|
||
updateConfigVisibility();
|
||
|
||
const updateModeBasedVisibility = () => {
|
||
if (!modeSelect) return;
|
||
const isSillyTavernMode = modeSelect.value === 'sillytavern_preset';
|
||
const isOpenAIMode = modeSelect.value === 'openai_test';
|
||
|
||
const presetContainer = presetSelect?.closest('.amily2_opt_settings_block');
|
||
if (presetContainer) {
|
||
presetContainer.style.display = isSillyTavernMode ? 'block' : 'none';
|
||
}
|
||
|
||
const fieldsToHideInPresetMode = [
|
||
{ element: urlInput, containerId: null },
|
||
{ element: keyInput, containerId: null },
|
||
{ element: modelInput, containerId: null },
|
||
{ element: maxTokensSlider, containerId: null },
|
||
{ element: temperatureSlider, containerId: null }
|
||
];
|
||
|
||
fieldsToHideInPresetMode.forEach(({ element }) => {
|
||
if (element) {
|
||
const container = element.closest('.amily2_opt_settings_block');
|
||
if (container) {
|
||
container.style.display = isSillyTavernMode ? 'none' : 'block';
|
||
}
|
||
}
|
||
});
|
||
|
||
const buttonsContainer = testButton?.closest('.nccs-button-row');
|
||
if (buttonsContainer) {
|
||
buttonsContainer.style.display = 'flex';
|
||
}
|
||
};
|
||
updateModeBasedVisibility();
|
||
|
||
enabledToggle.addEventListener('change', () => {
|
||
settings.nccsEnabled = enabledToggle.checked;
|
||
saveSettingsDebounced();
|
||
updateConfigVisibility();
|
||
log(`Nccs API ${enabledToggle.checked ? '已启用' : '已禁用'}`, 'info');
|
||
});
|
||
|
||
if (modeSelect) {
|
||
modeSelect.addEventListener('change', () => {
|
||
settings.nccsApiMode = modeSelect.value;
|
||
saveSettingsDebounced();
|
||
updateModeBasedVisibility();
|
||
log(`Nccs API模式已切换为: ${modeSelect.value}`, 'info');
|
||
});
|
||
}
|
||
|
||
if (urlInput) {
|
||
urlInput.addEventListener('blur', () => {
|
||
settings.nccsApiUrl = urlInput.value;
|
||
saveSettingsDebounced();
|
||
});
|
||
}
|
||
|
||
if (keyInput) {
|
||
keyInput.addEventListener('blur', () => {
|
||
settings.nccsApiKey = keyInput.value;
|
||
saveSettingsDebounced();
|
||
});
|
||
}
|
||
|
||
if (modelInput) {
|
||
modelInput.addEventListener('blur', () => {
|
||
settings.nccsModel = modelInput.value;
|
||
saveSettingsDebounced();
|
||
});
|
||
}
|
||
|
||
if (maxTokensSlider && maxTokensValue) {
|
||
maxTokensSlider.addEventListener('input', () => {
|
||
maxTokensValue.textContent = maxTokensSlider.value;
|
||
});
|
||
maxTokensSlider.addEventListener('change', () => {
|
||
settings.nccsMaxTokens = parseInt(maxTokensSlider.value);
|
||
saveSettingsDebounced();
|
||
});
|
||
}
|
||
|
||
if (temperatureSlider && temperatureValue) {
|
||
temperatureSlider.addEventListener('input', () => {
|
||
temperatureValue.textContent = temperatureSlider.value;
|
||
});
|
||
temperatureSlider.addEventListener('change', () => {
|
||
settings.nccsTemperature = parseFloat(temperatureSlider.value);
|
||
saveSettingsDebounced();
|
||
});
|
||
}
|
||
|
||
if (presetSelect) {
|
||
presetSelect.addEventListener('change', () => {
|
||
settings.nccsTavernProfile = presetSelect.value;
|
||
saveSettingsDebounced();
|
||
});
|
||
}
|
||
|
||
if (testButton) {
|
||
testButton.addEventListener('click', async () => {
|
||
testButton.disabled = true;
|
||
testButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 测试中...';
|
||
|
||
try {
|
||
const success = await testNccsApiConnection();
|
||
if (success) {
|
||
toastr.success('Nccs API连接测试成功!');
|
||
log('Nccs API连接测试成功', 'success');
|
||
} else {
|
||
toastr.error('Nccs API连接测试失败,请检查配置');
|
||
log('Nccs API连接测试失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
toastr.error('Nccs API连接测试出错:' + error.message);
|
||
log('Nccs API连接测试出错:' + error.message, 'error');
|
||
} finally {
|
||
testButton.disabled = false;
|
||
testButton.innerHTML = '<i class="fas fa-plug"></i> 测试连接';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (fetchModelsButton) {
|
||
fetchModelsButton.addEventListener('click', async () => {
|
||
fetchModelsButton.disabled = true;
|
||
fetchModelsButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 获取中...';
|
||
|
||
try {
|
||
const models = await fetchNccsModels();
|
||
if (models && models.length > 0) {
|
||
if (modelInput) {
|
||
modelInput.placeholder = `可用模型: ${models.slice(0, 3).join(', ')}...`;
|
||
}
|
||
toastr.success(`成功获取 ${models.length} 个模型`);
|
||
log(`Nccs API获取到 ${models.length} 个模型`, 'success');
|
||
} else {
|
||
toastr.warning('未获取到可用模型');
|
||
log('Nccs API未获取到可用模型', 'warn');
|
||
}
|
||
} catch (error) {
|
||
toastr.error('获取模型失败:' + error.message);
|
||
log('Nccs API获取模型失败:' + error.message, 'error');
|
||
} finally {
|
||
fetchModelsButton.disabled = false;
|
||
fetchModelsButton.innerHTML = '<i class="fas fa-download"></i> 获取模型';
|
||
}
|
||
});
|
||
}
|
||
|
||
const loadSillyTavernPresets = async () => {
|
||
if (!presetSelect) return;
|
||
try {
|
||
const context = getContext();
|
||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||
throw new Error('无法获取SillyTavern配置文件列表');
|
||
}
|
||
|
||
const profiles = context.extensionSettings.connectionManager.profiles;
|
||
|
||
presetSelect.innerHTML = '<option value="">选择预设</option>';
|
||
if (profiles && profiles.length > 0) {
|
||
profiles.forEach(profile => {
|
||
const option = document.createElement('option');
|
||
option.value = profile.id;
|
||
option.textContent = profile.name;
|
||
presetSelect.appendChild(option);
|
||
});
|
||
log(`成功加载 ${profiles.length} 个SillyTavern配置文件`, 'success');
|
||
} else {
|
||
log('未找到可用的SillyTavern配置文件', 'warn');
|
||
}
|
||
} catch (error) {
|
||
log('加载SillyTavern预设失败:' + error.message, 'error');
|
||
}
|
||
};
|
||
|
||
if (modeSelect && presetSelect) {
|
||
modeSelect.addEventListener('change', () => {
|
||
if (modeSelect.value === 'sillytavern_preset') {
|
||
loadSillyTavernPresets();
|
||
}
|
||
});
|
||
|
||
if (settings.nccsApiMode === 'sillytavern_preset') {
|
||
loadSillyTavernPresets();
|
||
}
|
||
}
|
||
|
||
log('Nccs API事件绑定完成', 'success');
|
||
}
|