From e7ecf4e4360edc5e3f64ecac1764d3ee1f179127 Mon Sep 17 00:00:00 2001 From: Wx-2025 <351320169@qq.com> Date: Tue, 6 Jan 2026 10:38:44 +0800 Subject: [PATCH] Update table-bindings.js --- ui/table-bindings.js | 2865 +++++++++++++++++++++++++++++++++--------- 1 file changed, 2256 insertions(+), 609 deletions(-) diff --git a/ui/table-bindings.js b/ui/table-bindings.js index 75bd1ba..2af54f8 100644 --- a/ui/table-bindings.js +++ b/ui/table-bindings.js @@ -1,654 +1,2301 @@ -import { getMemoryState, getHighlights } from '../core/table-system/manager.js'; -import { extension_settings } from '/scripts/extensions.js'; +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 { getContext } from '/scripts/extensions.js'; +import { updateOrInsertTableInChat } from './message-table-renderer.js'; +import { saveSettingsDebounced } from '/script.js'; +import { startBatchFilling } from '../core/table-system/batch-filler.js'; +import { showHtmlModal } from './page-window.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'; +import { showGraphVisualization } from '../core/relationship-graph/visualizer.js'; +import { escapeHTML } from '../utils/utils.js'; -const TABLE_CONTAINER_ID = 'amily2-chat-table-container'; const isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches; +const getAllTablesContainer = () => document.getElementById('all-tables-container'); -// 【V155.3】注入真正的游戏UI样式 (侧边栏+内容区) -function injectChatTableStyles() { - if (document.getElementById('amily2-chat-table-styles')) return; - const style = document.createElement('style'); - style.id = 'amily2-chat-table-styles'; - style.textContent = ` - /* 主容器:游戏面板风格 */ - #amily2-chat-table-container { - display: flex !important; /* 强制 Flex 布局 */ - flex-direction: row !important; /* 强制横向排列 */ - min-height: 300px; - max-height: 80vh; - background: rgba(12, 14, 20, 0.95); - border: 2px solid #3a4a5e; - border-radius: 8px; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.8), inset 0 0 30px rgba(0, 0, 0, 0.5); - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - color: #c0c0c0; - margin-top: 15px; - overflow: hidden; - position: relative; - resize: vertical; - } +let isResizing = false; +let activeTableIndex = 0; // 【V155.0】当前激活的表格索引 - /* 装饰性角落 */ - #amily2-chat-table-container::before { - content: ''; - position: absolute; - top: 0; left: 0; - width: 20px; height: 20px; - border-top: 2px solid #00bfff; - border-left: 2px solid #00bfff; - border-radius: 6px 0 0 0; - z-index: 2; - } - #amily2-chat-table-container::after { - content: ''; - position: absolute; - bottom: 0; right: 0; - width: 20px; height: 20px; - border-bottom: 2px solid #00bfff; - border-right: 2px solid #00bfff; - border-radius: 0 0 6px 0; - z-index: 2; - } - /* 侧边栏:导航菜单 */ - .amily2-game-sidebar { - width: 140px; /* 加宽以显示文字 */ - background: rgba(20, 25, 35, 0.9); - border-right: 1px solid #3a4a5e; - display: flex; - flex-direction: column; - align-items: stretch; /* 拉伸以填满宽度 */ - padding: 10px; - gap: 8px; - overflow-y: auto; - flex-shrink: 0; - } +function toggleRowContextMenu(event) { + event.preventDefault(); + event.stopPropagation(); - .amily2-game-tab { - height: 40px; - border-radius: 6px; - display: flex; - justify-content: flex-start; /* 左对齐 */ - align-items: center; - padding: 0 10px; - cursor: pointer; - color: #7a8a9e; - transition: all 0.2s ease; - position: relative; - border: 1px solid transparent; - font-size: 0.9em; - font-weight: 600; - } + const targetTd = event.target.closest('td.index-col'); + if (!targetTd) return; - .amily2-game-tab i { - width: 24px; - text-align: center; - margin-right: 8px; - } + const tableWrapper = targetTd.closest('.amily2-table-wrapper'); + if (!tableWrapper) return; - .amily2-game-tab:hover { - color: #e0e0e0; - background: rgba(255, 255, 255, 0.05); - } - - .amily2-game-tab.active { - color: #fff; - background: linear-gradient(90deg, rgba(0, 191, 255, 0.25), transparent); - border-left: 3px solid #00bfff; - text-shadow: 0 0 8px rgba(0, 191, 255, 0.8); - box-shadow: inset 5px 0 10px -5px rgba(0, 191, 255, 0.3); - } - - .amily2-game-tab.active::after { - display: none; /* 移除原来的三角形指示器 */ - } - - /* 内容区 */ - .amily2-game-content { - position: absolute; - left: 140px; top: 0; bottom: 0; right: 0; - overflow: hidden; - background: transparent; - display: flex; - flex-direction: column; - z-index: 10; - } - - /* 扫描线效果 */ - .amily2-game-content::before { - content: ''; - position: absolute; - top: 0; left: 0; right: 0; bottom: 0; - background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%); - background-size: 100% 4px; - pointer-events: none; - z-index: 1; - opacity: 0.3; - } - - .amily2-game-panel { - display: none; - width: 100%; - height: 100%; - padding: 20px; - overflow-y: auto; - box-sizing: border-box; - position: relative; - z-index: 10; /* 确保最高层级 */ - } - - .amily2-game-panel.active { - display: block !important; - animation: amily2-panel-fade 0.3s ease-out; - } - - @keyframes amily2-panel-fade { - from { opacity: 0; transform: translateY(5px); } - to { opacity: 1; transform: translateY(0); } - } - - /* 面板标题 */ - .amily2-panel-title { - font-size: 1.2em; - font-weight: bold; - color: #00bfff; - margin-bottom: 15px; - padding-bottom: 8px; - border-bottom: 2px solid rgba(0, 191, 255, 0.3); - text-transform: uppercase; - letter-spacing: 1px; - display: flex; - align-items: center; - } - - .amily2-panel-title i { - margin-right: 10px; - } - - /* 卡片式布局 (RPG风格) */ - .amily2-game-cards-container { - display: flex; - flex-direction: column; - gap: 10px; - } - - .amily2-game-card { - background: rgba(30, 35, 45, 0.6); - border: 1px solid rgba(100, 149, 237, 0.15); - border-radius: 6px; - padding: 12px; - position: relative; - transition: all 0.2s ease; - } - - .amily2-game-card:hover { - background: rgba(40, 50, 70, 0.8); - border-color: rgba(0, 191, 255, 0.4); - box-shadow: 0 0 15px rgba(0, 0, 0, 0.3); - transform: translateX(2px); - } - - .amily2-game-card::before { - content: ''; - position: absolute; - left: 0; top: 10px; bottom: 10px; - width: 3px; - background: #00bfff; - border-radius: 0 2px 2px 0; - opacity: 0.5; - } - - .amily2-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - padding-bottom: 5px; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - } - - .amily2-card-title { - font-size: 1.1em; - font-weight: bold; - color: #00bfff; - text-shadow: 0 0 5px rgba(0, 191, 255, 0.3); - } - - .amily2-card-body { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 8px 15px; - } - - .amily2-card-attr { - display: flex; - flex-direction: column; - font-size: 0.9em; - } - - .amily2-card-label { - color: #5a6a7e; - font-size: 0.8em; - text-transform: uppercase; - margin-bottom: 2px; - } - - .amily2-card-value { - color: #e0e0e0; - } - - /* 滚动条 */ - .amily2-game-sidebar::-webkit-scrollbar, - .amily2-game-panel::-webkit-scrollbar { - width: 4px; - } - .amily2-game-sidebar::-webkit-scrollbar-track, - .amily2-game-panel::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.2); - } - .amily2-game-sidebar::-webkit-scrollbar-thumb, - .amily2-game-panel::-webkit-scrollbar-thumb { - background: #3a4a5e; - border-radius: 2px; - } - - /* 移动端适配 */ - @media (max-width: 768px) { - #amily2-chat-table-container { - flex-direction: column; - height: auto; - min-height: 400px; - } - .amily2-game-sidebar { - width: 100% !important; - height: 50px !important; - flex-direction: row; - padding: 0 10px; - border-right: none; - border-bottom: 1px solid #3a4a5e; - overflow-x: auto; - top: 30px !important; - bottom: auto !important; - } - .amily2-game-content { - left: 0 !important; - top: 80px !important; - } - .amily2-game-tab { - flex-shrink: 0; - } - .amily2-game-tab.active::after { - right: auto; - bottom: -8px; - top: auto; - left: 50%; - transform: translateX(-50%) rotate(90deg); + const isActive = targetTd.classList.contains('amily2-menu-open'); + document.querySelectorAll('.amily2-menu-open').forEach(openEl => { + if (openEl !== targetTd) { + openEl.classList.remove('amily2-menu-open'); + openEl.style.zIndex = ''; + openEl.style.position = ''; + const otherWrapper = openEl.closest('.amily2-table-wrapper'); + if (otherWrapper) { + otherWrapper.style.overflowX = 'auto'; + otherWrapper.style.zIndex = ''; + otherWrapper.style.position = ''; } } - - /* 折叠功能样式 */ - #amily2-chat-table-container.collapsed { - min-height: 30px !important; - height: 30px !important; - resize: none !important; - overflow: hidden !important; - border-bottom: none; - } - - #amily2-chat-table-container.collapsed .amily2-game-sidebar, - #amily2-chat-table-container.collapsed .amily2-game-content { - display: none !important; - } - - .amily2-table-toggle { - position: absolute; - top: 0; left: 0; right: 0; - height: 30px; - background: rgba(20, 25, 35, 0.95); - border-bottom: 1px solid #3a4a5e; - color: #00bfff; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - z-index: 100; - font-size: 0.85em; - font-weight: bold; - text-transform: uppercase; - letter-spacing: 1px; - transition: all 0.2s; - } - - .amily2-table-toggle:hover { - background: rgba(40, 50, 70, 1); - color: #fff; - } - - .amily2-table-toggle i { - margin-right: 8px; - transition: transform 0.3s; - } - - #amily2-chat-table-container:not(.collapsed) .amily2-table-toggle i { - transform: rotate(180deg); - } - `; - document.head.appendChild(style); -} - -function getTableIcon(tableName) { - const lowerName = tableName.toLowerCase(); - if (lowerName.includes('时空') || lowerName.includes('时间') || lowerName.includes('time') || lowerName.includes('clock')) return 'fa-clock'; - if (lowerName.includes('角色') || lowerName.includes('人物') || lowerName.includes('char') || lowerName.includes('person')) return 'fa-user'; - if (lowerName.includes('关系') || lowerName.includes('relation') || lowerName.includes('social')) return 'fa-users'; - if (lowerName.includes('任务') || lowerName.includes('目标') || lowerName.includes('quest') || lowerName.includes('mission')) return 'fa-tasks'; - if (lowerName.includes('物品') || lowerName.includes('道具') || lowerName.includes('item') || lowerName.includes('inventory')) return 'fa-box-open'; - if (lowerName.includes('技能') || lowerName.includes('能力') || lowerName.includes('skill') || lowerName.includes('ability')) return 'fa-bolt'; - if (lowerName.includes('设定') || lowerName.includes('世界') || lowerName.includes('setting') || lowerName.includes('world')) return 'fa-book'; - if (lowerName.includes('总结') || lowerName.includes('大纲') || lowerName.includes('summary') || lowerName.includes('outline')) return 'fa-file-alt'; - if (lowerName.includes('日志') || lowerName.includes('log') || lowerName.includes('record')) return 'fa-clipboard-list'; - return 'fa-table'; -} - -function renderTablesToHtml(tables, highlights) { - if (!tables || tables.length === 0) { - return ''; - } - - // 过滤掉空表格 - const activeTables = tables.map((t, i) => ({...t, originalIndex: i})).filter(t => t.rows && t.rows.length > 0); - if (activeTables.length === 0) return ''; - - // Toggle 按钮 - const toggleHtml = ` -
- - 表格面板 / Data Panel -
- `; - - // 使用绝对定位强制布局,这是最稳妥的方式,不受 Flex 环境影响 - // top: 30px 留给 toggle 按钮 - let sidebarHtml = '
'; - let contentHtml = '
'; - - activeTables.forEach((table, index) => { - const isActive = index === 0 ? 'active' : ''; - const icon = getTableIcon(table.name); - - // 侧边栏按钮 (现在包含文字) - sidebarHtml += ` -
- - ${table.name} -
- `; - - // 内容面板 (卡片式渲染) - let cardsHtml = ''; - - // 如果是单行表格(如时空栏),使用特殊布局 - if (table.rows.length === 1) { - const row = table.rows[0]; - cardsHtml += `
-
- ${row.map((cell, colIndex) => { - const header = table.headers[colIndex]; - const highlightKey = `${table.originalIndex}-0-${colIndex}`; - const isHighlighted = highlights.has(highlightKey); - const style = isHighlighted ? 'style="color: #00ff7f;"' : ''; - return ` -
- ${header} - ${cell} -
- `; - }).join('')} -
-
`; - } else { - // 多行表格,每行一个卡片 - table.rows.forEach((row, rowIndex) => { - const rowStatus = table.rowStatuses ? table.rowStatuses[rowIndex] : 'normal'; - if (rowStatus === 'pending-deletion') return; - - // 假设第一列是标题/名称 - const titleCell = row[0]; - const otherCells = row.slice(1); - const otherHeaders = table.headers.slice(1); - - cardsHtml += ` -
-
- ${titleCell} - #${rowIndex + 1} -
-
- ${otherCells.map((cell, i) => { - const colIndex = i + 1; - const header = otherHeaders[i]; - const highlightKey = `${table.originalIndex}-${rowIndex}-${colIndex}`; - const isHighlighted = highlights.has(highlightKey); - const style = isHighlighted ? 'style="color: #00ff7f;"' : ''; - return ` -
- ${header} - ${cell} -
- `; - }).join('')} -
-
- `; - }); - } - - contentHtml += ` -
-
${table.name}
-
- ${cardsHtml} -
-
- `; }); - sidebarHtml += '
'; - contentHtml += '
'; + targetTd.classList.toggle('amily2-menu-open'); - return `
${toggleHtml}${sidebarHtml}${contentHtml}
`; -} + if (targetTd.classList.contains('amily2-menu-open')) { + tableWrapper.style.overflowX = 'visible'; + tableWrapper.style.position = 'relative'; + tableWrapper.style.zIndex = '10'; + targetTd.style.position = 'relative'; + targetTd.style.zIndex = '100'; + } else { + tableWrapper.style.overflowX = 'auto'; + tableWrapper.style.position = ''; + tableWrapper.style.zIndex = ''; + targetTd.style.position = ''; + targetTd.style.zIndex = ''; + } -function removeTableContainer() { - const existingContainer = document.getElementById(TABLE_CONTAINER_ID); - if (existingContainer) { - existingContainer.remove(); + const closeMenu = (e) => { + if (!targetTd.contains(e.target)) { + targetTd.classList.remove('amily2-menu-open'); + targetTd.style.position = ''; + targetTd.style.zIndex = ''; + tableWrapper.style.overflowX = 'auto'; + tableWrapper.style.position = ''; + tableWrapper.style.zIndex = ''; + document.removeEventListener('click', closeMenu, true); + } + }; + + if (targetTd.classList.contains('amily2-menu-open')) { + setTimeout(() => { + document.addEventListener('click', closeMenu, true); + }, 0); } } -function bindSwipePreventer(container) { - if (!isTouchDevice()) { + +function toggleColumnContextMenu(event) { + if (isResizing || event.target.classList.contains('amily2-resizer')) { return; } + event.preventDefault(); + event.stopPropagation(); - let touchstartX = 0; - let touchstartY = 0; + const targetTh = event.target.closest('th'); + if (!targetTh) return; - container.addEventListener('touchstart', function(event) { - touchstartX = event.changedTouches[0].screenX; - touchstartY = event.changedTouches[0].screenY; - }, { passive: true }); + const tableWrapper = targetTh.closest('.amily2-table-wrapper'); + if (!tableWrapper) return; - container.addEventListener('touchmove', function(event) { - const touchendX = event.changedTouches[0].screenX; - const touchendY = event.changedTouches[0].screenY; + const isActive = targetTh.classList.contains('amily2-menu-open'); - const deltaX = Math.abs(touchendX - touchstartX); - const deltaY = Math.abs(touchendY - touchstartY); - - if (deltaX > deltaY) { - event.stopPropagation(); + document.querySelectorAll('th.amily2-menu-open').forEach(openTh => { + if (openTh !== targetTh) { + openTh.classList.remove('amily2-menu-open'); + const otherWrapper = openTh.closest('.amily2-table-wrapper'); + if (otherWrapper) { + otherWrapper.style.overflowX = 'auto'; + otherWrapper.style.zIndex = ''; + otherWrapper.style.position = ''; + } } - }, { passive: false }); + }); + + targetTh.classList.toggle('amily2-menu-open'); + + if (targetTh.classList.contains('amily2-menu-open')) { + tableWrapper.style.overflowX = 'visible'; + tableWrapper.style.position = 'relative'; + tableWrapper.style.zIndex = '10'; + } else { + tableWrapper.style.overflowX = 'auto'; + tableWrapper.style.position = ''; + tableWrapper.style.zIndex = ''; + } + + const closeMenu = (e) => { + if (!targetTh.contains(e.target)) { + targetTh.classList.remove('amily2-menu-open'); + tableWrapper.style.overflowX = 'auto'; + tableWrapper.style.position = ''; + tableWrapper.style.zIndex = ''; + document.removeEventListener('click', closeMenu, true); + } + }; + + if (targetTh.classList.contains('amily2-menu-open')) { + setTimeout(() => { + document.addEventListener('click', closeMenu, true); + }, 0); + } } -export function updateOrInsertTableInChat() { - injectChatTableStyles(); // 确保样式已注入 + +function toggleHeaderIndexContextMenu(event) { + event.preventDefault(); + event.stopPropagation(); + + const targetTh = event.target.closest('th.index-col'); + if (!targetTh) return; + + const menu = targetTh.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(() => { - const context = getContext(); - if (!context || !context.chat || context.chat.length < 2) { - removeTableContainer(); - return; - } - - const settings = extension_settings[extensionName]; - removeTableContainer(); - - if (!settings || !settings.show_table_in_chat) { - return; - } - - const tables = getMemoryState(); - - if (!tables || tables.every(t => !t.rows || t.rows.length === 0)) { - return; - } - - const highlights = getHighlights(); - const htmlContent = renderTablesToHtml(tables, highlights); - - if (!htmlContent) { - return; - } - - const lastMessage = document.querySelector('.last_mes .mes_text'); - if (lastMessage) { - const container = document.createElement('div'); - container.id = TABLE_CONTAINER_ID; - - // 强制内联样式,使用相对定位作为绝对定位子元素的锚点 - container.style.cssText = ` - display: block !important; /* 不再依赖 Flex */ - min-height: 300px; - max-height: 80vh; - background: rgba(12, 14, 20, 0.95); - border: 2px solid #3a4a5e; - border-radius: 8px; - margin-top: 15px; - overflow: hidden; - position: relative; /* 关键:作为定位锚点 */ - resize: vertical; - width: 100%; - `; - - container.innerHTML = htmlContent; - container.classList.add('collapsed'); // 默认折叠 - - // On mobile devices, add a specific class to enable horizontal scrolling via CSS - if (isTouchDevice()) { - container.classList.add('mobile-table-view'); - container.style.flexDirection = 'column'; // 移动端保持垂直 - } - - lastMessage.appendChild(container); - bindSwipePreventer(container); - - // 绑定折叠按钮事件 - const toggleBtn = container.querySelector('.amily2-table-toggle'); - if (toggleBtn) { - toggleBtn.addEventListener('click', (e) => { - e.stopPropagation(); - container.classList.toggle('collapsed'); - }); - } - - // 【V155.3】绑定游戏UI的交互事件 - const tabs = container.querySelectorAll('.amily2-game-tab'); - const panels = container.querySelectorAll('.amily2-game-panel'); - - tabs.forEach(tab => { - tab.addEventListener('click', (e) => { - e.stopPropagation(); // 防止触发消息点击 - - // 移除所有激活状态 - tabs.forEach(t => t.classList.remove('active')); - panels.forEach(p => p.classList.remove('active')); - - // 激活当前 - tab.classList.add('active'); - const targetId = tab.dataset.target; - const targetPanel = container.querySelector(`#${targetId}`); - if (targetPanel) targetPanel.classList.add('active'); - }); - }); - - } else { - console.warn('[Amily2] 未找到最后一条消息的容器,无法插入表格。'); + if (menu.classList.contains('amily2-menu-active')) { + document.addEventListener('click', closeMenu, true); } }, 0); } -function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); + +function showInputDialog({ title, label, currentValue, placeholder, onSave }) { + const dialogHtml = ` + + + `; + + 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(); } -let chatObserver = null; -const debouncedUpdate = debounce(updateOrInsertTableInChat, 100); -export function startContinuousRendering() { - if (chatObserver) { - console.log('[Amily2] Continuous rendering is already active.'); - return; - } - - const chatContainer = document.getElementById('chat'); - if (!chatContainer) { - console.error('[Amily2] Could not find chat container to observe.'); - setTimeout(startContinuousRendering, 500); - return; - } - - const observerConfig = { childList: true }; - - chatObserver = new MutationObserver((mutationsList, observer) => { - for (const mutation of mutationsList) { - if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { - let messageAdded = false; - mutation.addedNodes.forEach(node => { - if (node.nodeType === 1 && node.classList.contains('mes')) { - messageAdded = true; - } - }); - - if (messageAdded) { - debouncedUpdate(); - return; - } - } +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; + } + + // 【V155.0】验证 activeTableIndex + if (activeTableIndex >= tables.length) { + activeTableIndex = Math.max(0, tables.length - 1); + } + + const highlights = TableManager.getHighlights(); + const updatedTables = TableManager.getUpdatedTables(); + const fragment = document.createDocumentFragment(); + + // 【V155.1 移动端适配】注入样式 + if (!document.getElementById('amily2-table-tabs-style')) { + const style = document.createElement('style'); + style.id = 'amily2-table-tabs-style'; + style.textContent = ` + .amily2-table-tabs { + display: flex; + overflow-x: auto; + gap: 8px; + margin-bottom: 10px; + padding-bottom: 8px; + border-bottom: 1px solid rgba(255,255,255,0.1); + align-items: center; + -webkit-overflow-scrolling: touch; /* iOS 平滑滚动 */ + scrollbar-width: none; /* Firefox 隐藏滚动条 */ + } + .amily2-table-tabs::-webkit-scrollbar { + display: none; /* Chrome/Safari 隐藏滚动条 */ + } + .amily2-table-tabs .menu_button { + flex-shrink: 0; /* 防止标签被压缩 */ + white-space: nowrap; + } + /* 移动端表头适配 */ + @media (max-width: 768px) { + .amily2-table-header-container { + flex-wrap: wrap; + gap: 8px; + } + .amily2-table-header-container h3 { + width: 100%; + margin-bottom: 5px; + } + .amily2-table-header-container .table-controls { + width: 100%; + justify-content: space-between; + } + .amily2-table-header-container .table-controls .menu_button { + flex: 1; + justify-content: center; + } + } + `; + document.head.appendChild(style); + } + + // 1. 渲染标签页 (Tabs) + const tabsContainer = document.createElement('div'); + tabsContainer.className = 'amily2-table-tabs'; + // 移除内联样式,改用上方注入的 CSS 类 + // tabsContainer.style.cssText = ... + + tables.forEach((table, index) => { + const tab = document.createElement('button'); + tab.className = `menu_button small_button ${index === activeTableIndex ? 'active' : ''}`; + tab.style.whiteSpace = 'nowrap'; + + // 高亮当前标签 + if (index === activeTableIndex) { + tab.style.backgroundColor = 'rgba(158, 138, 255, 0.4)'; + tab.style.borderColor = 'rgba(158, 138, 255, 0.8)'; + tab.style.boxShadow = '0 0 8px rgba(158, 138, 255, 0.3)'; + } + + // 如果表格有更新,添加标记 + if (updatedTables.has(index)) { + tab.innerHTML = `${escapeHTML(table.name)} `; + } else { + tab.textContent = table.name; + } + + tab.onclick = () => { + activeTableIndex = index; + renderTables(); + }; + tabsContainer.appendChild(tab); + }); + + // 添加“新建表格”按钮到标签栏 + const addBtn = document.createElement('button'); + addBtn.className = 'menu_button small_button'; + addBtn.innerHTML = ''; + addBtn.title = '新建表格'; + addBtn.style.marginLeft = '5px'; + addBtn.onclick = () => { + const newName = prompt('请输入新表格的名称:', '新表格'); + if (newName && newName.trim()) { + TableManager.addTable(newName.trim()); + // 切换到新创建的表格 + const newTables = TableManager.getMemoryState(); + activeTableIndex = newTables.length - 1; + renderTables(); + } + }; + tabsContainer.appendChild(addBtn); + + fragment.appendChild(tabsContainer); + + // 2. 渲染当前激活的表格 (Active Table) + if (tables.length > 0 && tables[activeTableIndex]) { + const tableIndex = activeTableIndex; + const tableData = tables[tableIndex]; + + const header = document.createElement('div'); + header.className = 'amily2-table-header-container'; + const title = document.createElement('h3'); + if (updatedTables.has(tableIndex)) { + title.classList.add('table-updated'); + } + title.innerHTML = ` ${escapeHTML(tableData.name)}`; + const controls = document.createElement('div'); + controls.className = 'table-controls'; + + // 左右移动表格(原上下移动) + // 【移动端优化】增加按钮的触摸区域和间距 + const moveLeftBtn = tableIndex > 0 ? `` : ''; + const moveRightBtn = tableIndex < tables.length - 1 ? `` : ''; + + controls.innerHTML = ` + ${moveLeftBtn} + ${moveRightBtn} + + + `; + header.appendChild(title); + header.appendChild(controls); + fragment.appendChild(header); + + const tableWrapper = document.createElement('div'); + tableWrapper.className = 'amily2-table-wrapper'; + + const tableElement = document.createElement('table'); + tableElement.id = `amily2-table-${tableIndex}`; + tableElement.dataset.tableIndex = tableIndex; + + const colgroup = document.createElement('colgroup'); + const indexCol = document.createElement('col'); + indexCol.style.width = '40px'; + colgroup.appendChild(indexCol); + + if (tableData.headers) { + tableData.headers.forEach((_, colIndex) => { + const col = document.createElement('col'); + const colWidth = (tableData.columnWidths && tableData.columnWidths[colIndex]) ? tableData.columnWidths[colIndex] : 90; + col.style.width = `${colWidth}px`; + colgroup.appendChild(col); + }); + } + tableElement.appendChild(colgroup); + + let totalWidth = 0; + const cols = colgroup.querySelectorAll('col'); + cols.forEach(col => { + totalWidth += parseInt(col.style.width, 10); + }); + tableElement.style.minWidth = '100%'; + if (totalWidth > 0) { + tableElement.style.width = `${Math.max(totalWidth, 0)}px`; + tableElement.style.minWidth = `${totalWidth}px`; + tableElement.style.width = '100%'; + } + + const thead = tableElement.createTHead(); + const headerRow = thead.insertRow(); + + const indexTh = document.createElement('th'); + indexTh.className = 'index-col'; + indexTh.textContent = '#'; + indexTh.style.cursor = 'pointer'; + indexTh.title = '点击添加第一行'; + + if (!tableData.rows || tableData.rows.length === 0) { + const headerMenu = document.createElement('div'); + headerMenu.className = 'amily2-context-menu amily2-header-menu'; + headerMenu.style.display = 'none'; // 默认隐藏 + + const addRowButton = document.createElement('button'); + addRowButton.innerHTML = ' 创建第一行'; + addRowButton.className = 'menu_button small_button'; + addRowButton.addEventListener('click', (e) => { + e.stopPropagation(); + TableManager.addRow(tableIndex); + renderTables(); + }); + + headerMenu.appendChild(addRowButton); + indexTh.appendChild(headerMenu); + + indexTh.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log('Header # clicked for table', tableIndex); + + TableManager.addRow(tableIndex); + renderTables(); + toastr.success('已添加第一行'); + }); + } + + 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; // textContent is safe + 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); + + const resizer = document.createElement('div'); + resizer.className = 'amily2-resizer'; + th.appendChild(resizer); + + const startResize = (startEvent) => { + startEvent.preventDefault(); + startEvent.stopPropagation(); + + isResizing = true; + + const table = startEvent.target.closest('table'); + const th = startEvent.target.parentElement; + const col = table.querySelector(`colgroup > col:nth-child(${th.cellIndex + 1})`); + + const isTouchEvent = startEvent.type.startsWith('touch'); + const startX = isTouchEvent ? startEvent.touches[0].clientX : startEvent.clientX; + const startWidth = th.offsetWidth; + + const onMove = (moveEvent) => { + const currentX = isTouchEvent ? moveEvent.touches[0].clientX : moveEvent.clientX; + const newWidth = startWidth + (currentX - startX); + if (newWidth > 50) { + col.style.width = `${newWidth}px`; + } + }; + + const onEnd = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onEnd); + document.removeEventListener('touchmove', onMove); + document.removeEventListener('touchend', onEnd); + + const finalWidth = parseInt(col.style.width, 10); + TableManager.updateColumnWidth(tableIndex, colIndex, finalWidth); + + setTimeout(() => { isResizing = false; }, 0); + }; + + if (isTouchEvent) { + document.addEventListener('touchmove', onMove, { passive: false }); + document.addEventListener('touchend', onEnd); + } else { + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onEnd); + } + }; + + resizer.addEventListener('mousedown', startResize); + resizer.addEventListener('touchstart', startResize, { passive: false }); + + 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 rowStatus = tableData.rowStatuses ? tableData.rowStatuses[rowIndex] : 'normal'; + if (rowStatus === 'pending-deletion') { + row.classList.add('pending-deletion-row'); + } + + 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'; + + let actions; + + if (rowStatus === 'pending-deletion') { + actions = [ + { label: '恢复该行', action: 'restore-row', icon: 'fa-undo', isSuccess: true, btnClass: 'restore-row-btn' } + ]; + } else { + actions = [ + { label: '向上移动', action: 'move-up', icon: 'fa-arrow-up', btnClass: 'move-row-up-btn' }, + { label: '向下移动', action: 'move-down', icon: 'fa-arrow-down', btnClass: 'move-row-down-btn' }, + { label: '在上加行', action: 'add-above', icon: 'fa-plus-circle', btnClass: 'add-row-above-btn' }, + { label: '在下加行', action: 'add-below', icon: 'fa-plus-circle', btnClass: 'add-row-below-btn' }, + { label: '删除该行', action: 'delete-row', icon: 'fa-trash-alt', isDanger: true, btnClass: 'delete-row-btn' } + ]; + } + + actions.forEach(({ label, action, icon, isDanger, isSuccess }) => { + const button = document.createElement('button'); + button.innerHTML = ` ${label}`; + button.className = 'menu_button small_button'; + if (isDanger) button.classList.add('danger'); + if (isSuccess) button.classList.add('success'); // Use a success style for restore + + 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': + TableManager.deleteRow(tableIndex, rowIndex); + break; + case 'restore-row': + TableManager.restoreRow(tableIndex, rowIndex); + break; + } + if (action === 'delete-row' || action === 'restore-row') { + } else { + renderTables(); + } + }); + menu.appendChild(button); + }); + indexCell.appendChild(menu); + + rowData.forEach((cellData, colIndex) => { + const cell = row.insertCell(); + + const cellContent = document.createElement('div'); + cellContent.className = 'amily2-cell-content'; + cellContent.textContent = cellData; + cell.appendChild(cellContent); + + if (rowStatus !== 'pending-deletion' && !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'); + } + }); + }); + } + tableWrapper.appendChild(tableElement); + fragment.appendChild(tableWrapper); + } else { + // 如果没有表格,显示占位符 + const placeholder = document.createElement('div'); + placeholder.id = 'add-table-placeholder'; + placeholder.innerHTML = ''; + placeholder.title = '点击创建第一个表格'; + placeholder.addEventListener('click', () => { + const newName = prompt('请输入新表格的名称:', '新表格'); + if (newName && newName.trim()) { + TableManager.addTable(newName.trim()); + renderTables(); + } + }); + fragment.appendChild(placeholder); + } + + container.innerHTML = ''; + container.appendChild(fragment); - chatObserver.observe(chatContainer, observerConfig); - console.log('[Amily2] Started continuous table rendering.'); updateOrInsertTableInChat(); } -export function stopContinuousRendering() { - if (chatObserver) { - chatObserver.disconnect(); - chatObserver = null; - removeTableContainer(); - console.log('[Amily2] Stopped continuous table rendering.'); + +function openTableRuleEditor() { + const settings = extension_settings[extensionName]; + const tags = settings.table_tags_to_extract || ''; + const exclusionRules = settings.table_exclusion_rules || []; + + const rulesHtml = exclusionRules.map((rule, index) => ` +
+ + - + + +
+ `).join(''); + + const modalHtml = ` +
+
+ + + 仅提取指定XML标签的内容,例如填“content”,即提取...中的内容。 +
+
+ +
${rulesHtml}
+ + 移除所有被起始和结束标记包裹的内容(例如 OOC 部分)。 +
+
+ `; + + const dialog = showHtmlModal('配置独立提取规则', modalHtml, { + onOk: () => { + const newTags = document.getElementById('table-tags-input').value; + updateAndSaveTableSetting('table_tags_to_extract', newTags); + + const newExclusionRules = []; + document.querySelectorAll('#exclusion-rules-list .exclusion-rule-item').forEach(item => { + const start = item.querySelector('.rule-start').value.trim(); + const end = item.querySelector('.rule-end').value.trim(); + if (start && end) { + newExclusionRules.push({ start, end }); + } + }); + updateAndSaveTableSetting('table_exclusion_rules', newExclusionRules); + toastr.success('独立提取规则已保存。'); + }, + onShow: (dialogElement) => { + const rulesList = dialogElement.find('#exclusion-rules-list'); + + dialogElement.find('#add-exclusion-rule-btn').on('click', () => { + const newIndex = rulesList.children().length; + const newItemHtml = ` +
+ + - + + +
`; + rulesList.append(newItemHtml); + }); + + rulesList.on('click', '.remove-rule-btn', function() { + $(this).closest('.exclusion-rule-item').remove(); + }); + } + }); +} + +function openRuleEditor(tableIndex) { + const tables = TableManager.getMemoryState(); + if (!tables || !tables[tableIndex]) return; + const table = tables[tableIndex]; + + if (table.charLimitRule && !table.charLimitRules) { + table.charLimitRules = {}; + if (table.charLimitRule.columnIndex !== -1) { + table.charLimitRules[table.charLimitRule.columnIndex] = table.charLimitRule.limit; + } + } + const charLimitRules = table.charLimitRules || {}; + + const renderCharLimitRules = (rules) => { + return Object.entries(rules).map(([colIndex, limit]) => { + const header = table.headers[colIndex] || `未知列 (${colIndex})`; + return ` +
+ ${escapeHTML(header)}: 不超过 ${limit} 字 + +
+ `; + }).join(''); + }; + + const getColumnOptions = (rules) => { + return table.headers.map((header, index) => { + if (rules[index]) return ''; + return ``; + }).join(''); + }; + + const dialogHtml = ` + + + `; + + const dialogElement = $(dialogHtml).appendTo('body'); + + const closeDialog = () => { + dialogElement[0].close(); + dialogElement.remove(); + }; + + const refreshRuleUI = () => { + const currentRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}'); + dialogElement.find('#current-char-limit-rules').html(renderCharLimitRules(currentRules)); + dialogElement.find('#new-rule-column-select').html(`${getColumnOptions(currentRules)}`); + }; + + dialogElement.find('#current-char-limit-rules').attr('data-rules', JSON.stringify(charLimitRules)); + + dialogElement.on('click', '#add-char-limit-rule-btn', () => { + const selectedColumn = parseInt(dialogElement.find('#new-rule-column-select').val(), 10); + const limitValue = parseInt(dialogElement.find('#new-rule-limit-input').val(), 10); + + if (selectedColumn === -1) { + toastr.warning('请选择一个列。'); + return; + } + + if (isNaN(limitValue) || limitValue < 0) { + toastr.warning('请输入一个有效的字数限制(大于等于0)。'); + return; + } + + const currentRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}'); + + if (limitValue > 0) { + currentRules[selectedColumn] = limitValue; + dialogElement.find('#current-char-limit-rules').attr('data-rules', JSON.stringify(currentRules)); + refreshRuleUI(); + } else { + toastr.info('字数限制为0表示不设置规则。'); + } + }); + + dialogElement.on('click', '.remove-char-limit-rule-btn', function() { + const colIndexToRemove = $(this).data('col-index'); + const currentRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}'); + delete currentRules[colIndexToRemove]; + dialogElement.find('#current-char-limit-rules').attr('data-rules', JSON.stringify(currentRules)); + refreshRuleUI(); + }); + + dialogElement.find('.popup-button-ok').on('click', () => { + const newCharLimitRules = JSON.parse(dialogElement.find('#current-char-limit-rules').attr('data-rules') || '{}'); + const rowLimitValue = parseInt(dialogElement.find('#rule-row-limit-value').val(), 10); + const simplifyThresholdValue = parseInt(dialogElement.find('#rule-simplify-threshold').val(), 10); + + 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(), + charLimitRules: newCharLimitRules, + rowLimitRule: rowLimitValue, + simplifyRowThreshold: simplifyThresholdValue, // 保存新设置 + }; + 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 optimizationCheckbox = document.getElementById('context-optimization-enabled'); // 【V144.0】 + 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; + if (optimizationCheckbox) { // 【V144.0】 + optimizationCheckbox.checked = settings.context_optimization_enabled !== false; + } + 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(); + + if (masterSwitchCheckbox.dataset.eventsBound) return; + + 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(); + }); + + // 【V144.0】 + if (optimizationCheckbox) { + optimizationCheckbox.addEventListener('change', () => { + settings.context_optimization_enabled = optimizationCheckbox.checked; + saveSettingsDebounced(); + toastr.info(`上下文优化(世界书合并)已${optimizationCheckbox.checked ? '启用' : '禁用'}。`); + }); + } + + 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(); + } + }); + }); + + masterSwitchCheckbox.dataset.eventsBound = 'true'; + 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 = '

加载条目中...

'; + 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 = '

获取角色世界书失败。

'; + return; + } + } else { + entryListContainer.innerHTML = '

请先加载一个角色。

'; + return; + } + } + + if (bookNames.length === 0) { + entryListContainer.innerHTML = '

未选择或绑定世界书。

'; + 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 = '

所选世界书中没有条目。

'; + 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 || '无标题条目'; // textContent is safe + + div.appendChild(checkbox); + div.appendChild(label); + entryListContainer.appendChild(div); + }); + } catch (error) { + console.error(`[内存储司] 加载世界书条目失败:`, error); + entryListContainer.innerHTML = '

加载条目失败。

'; + } + }; + + 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; // textContent is safe + + div.appendChild(checkbox); + div.appendChild(label); + bookListContainer.appendChild(div); + }); + } else { + bookListContainer.innerHTML = '

没有找到世界书。

'; + } + 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(); + + if (enabledCheckbox.dataset.eventsBound) return; + + 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(); + } + }); + + enabledCheckbox.dataset.eventsBound = 'true'; + 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 secondaryFillerControls = document.getElementById('secondary-filler-controls'); + + const contextSlider = document.getElementById('secondary-filler-context'); + const batchSlider = document.getElementById('secondary-filler-batch'); + const bufferSlider = document.getElementById('secondary-filler-buffer'); + + const independentRulesContainer = document.getElementById('table-independent-rules-container'); + const independentRulesToggle = document.getElementById('table-independent-rules-enabled'); + const configureRulesBtn = document.getElementById('table-configure-rules-btn'); + + const updateFillingModeUI = () => { + const currentMode = extension_settings[extensionName]?.filling_mode || 'main-api'; + fillingModeRadios.forEach(radio => { + radio.checked = (radio.value === currentMode); + }); + + const isSecondaryMode = currentMode === 'secondary-api'; + + if (secondaryFillerControls) { + secondaryFillerControls.style.display = isSecondaryMode ? 'block' : 'none'; + } + + if (independentRulesContainer) { + independentRulesContainer.style.display = 'flex'; + } + + if (independentRulesToggle && configureRulesBtn) { + configureRulesBtn.style.display = independentRulesToggle.checked ? 'block' : '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(); + }); + }); + + if (contextSlider) { + const value = extension_settings[extensionName]?.secondary_filler_context || 2; + contextSlider.value = value; + + contextSlider.addEventListener('change', function() { + updateAndSaveTableSetting('secondary_filler_context', parseInt(this.value, 10)); + toastr.info(`上下文深度已设置为 ${this.value}。`); + }); + } + + if (batchSlider) { + const value = extension_settings[extensionName]?.secondary_filler_batch || 0; + batchSlider.value = value; + + batchSlider.addEventListener('change', function() { + updateAndSaveTableSetting('secondary_filler_batch', parseInt(this.value, 10)); + toastr.info(`填表批次已设置为 ${this.value}。`); + }); + } + + if (bufferSlider) { + const value = extension_settings[extensionName]?.secondary_filler_buffer || 0; + bufferSlider.value = value; + + bufferSlider.addEventListener('change', function() { + updateAndSaveTableSetting('secondary_filler_buffer', parseInt(this.value, 10)); + toastr.info(`保留楼层已设置为 ${this.value}。`); + }); + } + + if (independentRulesToggle) { + independentRulesToggle.checked = extension_settings[extensionName]?.table_independent_rules_enabled ?? false; + independentRulesToggle.addEventListener('change', () => { + updateAndSaveTableSetting('table_independent_rules_enabled', independentRulesToggle.checked); + updateFillingModeUI(); + }); + } + + updateFillingModeUI(); + + if (configureRulesBtn) { + configureRulesBtn.addEventListener('click', openTableRuleEditor); + } + + const renderAll = () => { + renderTables(); + bindInjectionSettings(); + bindTemplateEditors(); + }; + + renderAll(); + bindWorldBookSettings(); + bindBatchFillButton(); // 【新增】绑定批量填表按钮 + bindFloorFillButtons(); // 【新增】绑定楼层填表按钮 + bindReorganizeButton(); // 【新增】绑定重新整理按钮 + bindClearRecordsButton(); // 【新增】绑定清除记录按钮 + bindNccsApiEvents(); // 【新增】绑定Nccs API系统事件 + bindChatTableDisplaySetting(); // 【新增】绑定聊天内表格显示开关 + + 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 openGraphBtn = document.getElementById('amily2-open-relationship-graph-btn'); + 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 (openGraphBtn) { + openGraphBtn.addEventListener('click', () => { + showGraphVisualization(); + }); + } + + 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(); + } + }); + } + + + // 【V155.0】移除旧的 addTablePlaceholder 绑定,因为现在它在 renderTables 内部动态生成 + // const addTablePlaceholder = document.getElementById('add-table-placeholder'); + // if (addTablePlaceholder) { ... } + + + const allTablesContainer = getAllTablesContainer(); + if (allTablesContainer) { + allTablesContainer.addEventListener('click', (event) => { + const th = event.target.closest('th'); + if (th && th.classList.contains('index-col')) { + toggleHeaderIndexContextMenu(event); + return; + } + 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); + // 【V155.0】移动表格后,activeTableIndex 需要跟随移动 + if (direction === 'up' && activeTableIndex > 0) { + activeTableIndex--; + } else if (direction === 'down' && activeTableIndex < TableManager.getMemoryState().length - 1) { + activeTableIndex++; + } + 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(); + } + } + }); + + 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; + + 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; + + // Correctly save scroll positions before re-rendering + const tableWrapper = tableElement.closest('.amily2-table-wrapper'); + const hScroll = tableWrapper ? tableWrapper.scrollLeft : 0; + const vScroll = allTablesContainer.scrollTop; + + TableManager.addHighlight(tableIndex, rowIndex, colIndex); + const dataToUpdate = { [colIndex]: newValue }; + TableManager.updateRow(tableIndex, rowIndex, dataToUpdate); + + renderAll(); + + // Correctly restore scroll positions after re-rendering + const newTableWrapper = document.getElementById(`amily2-table-${tableIndex}`)?.closest('.amily2-table-wrapper'); + if (newTableWrapper) { + newTableWrapper.scrollLeft = hScroll; + } + allTablesContainer.scrollTop = vScroll; + + }, true); + } + + 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; + } + + const tables = TableManager.getMemoryState(); + if (!tables || tables.length === 0) { + toastr.warning('当前没有表格可供整理。'); + return; + } + + // 构建表格选择列表 HTML + const tableListHtml = tables.map((table, index) => ` +
+ + +
+ `).join(''); + + const modalHtml = ` +
+

建议:最好一次只选择一个表格,或少数几个相关联的表格进行整理。一次性处理过多表格可能会导致AI混淆或遗漏信息。

+

请勾选需要AI重新整理和去重的表格:

+
+ ${tableListHtml} +
+
+ + +
+
+ `; + + showHtmlModal('选择要整理的表格', modalHtml, { + onOk: async (dialogElement) => { + const selectedIndices = []; + dialogElement.find('input[type="checkbox"]:checked').each(function() { + selectedIndices.push(parseInt($(this).val(), 10)); + }); + + if (selectedIndices.length === 0) { + toastr.warning('请至少选择一个表格。'); + return false; // 阻止关闭弹窗 + } + + try { + const { reorganizeTableContent } = await import('../core/table-system/reorganizer.js'); + await reorganizeTableContent(selectedIndices); + } catch (error) { + console.error('[内存储司] 重新整理功能导入失败:', error); + toastr.error('重新整理功能启动失败,请检查系统状态。'); + } + }, + onShow: (dialogElement) => { + dialogElement.find('#reorg-select-all').on('click', () => { + dialogElement.find('input[type="checkbox"]').prop('checked', true); + }); + dialogElement.find('#reorg-deselect-all').on('click', () => { + dialogElement.find('input[type="checkbox"]').prop('checked', false); + }); + } + }); + }); + + reorganizeBtn.dataset.reorganizeEventBound = 'true'; + log('"重新整理"按钮已成功绑定。', 'success'); + } +} + +function bindClearRecordsButton() { + const clearBtn = document.getElementById('clear-records-btn'); + const floorInput = document.getElementById('clear-records-before-floor'); + + if (clearBtn && floorInput) { + if (clearBtn.dataset.clearEventBound) return; + + clearBtn.addEventListener('click', async () => { + const floorIndex = parseInt(floorInput.value, 10); + if (isNaN(floorIndex) || floorIndex < 0) { + toastr.warning('请输入有效的楼层号。'); + return; + } + + if (confirm(`【警告】您确定要清除第 ${floorIndex} 楼之前的所有表格记录吗?\n\n此操作将永久删除这些消息中存储的表格快照,无法恢复。当前最新的表格状态不会受影响。`)) { + try { + const { clearTableRecordsBefore } = await import('../core/table-system/cleaner.js'); + const count = await clearTableRecordsBefore(floorIndex); + toastr.success(`已成功清除 ${count} 条消息中的表格记录。`); + } catch (error) { + console.error('[内存储司] 清除记录失败:', error); + toastr.error('清除记录失败,请检查控制台日志。'); + } + } + }); + + clearBtn.dataset.clearEventBound = 'true'; + log('"清除记录"按钮已成功绑定。', 'success'); + } +} + + +function bindFloorFillButtons() { + const selectedFloorsBtn = document.getElementById('fill-selected-floors-btn'); + const currentFloorBtn = document.getElementById('fill-current-floor-btn'); + const rollbackBtn = document.getElementById('rollback-and-refill-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'); + } + + if (rollbackBtn) { + if (rollbackBtn.dataset.rollbackEventBound) return; + + rollbackBtn.addEventListener('click', async (event) => { + const settings = extension_settings[extensionName]; + const tableSystemEnabled = settings.table_system_enabled !== false; + + if (!tableSystemEnabled) { + event.preventDefault(); + toastr.warning('表格系统总开关已关闭,请先启用总开关。'); + return; + } + + if (confirm('您确定要将表格状态回退到上一楼,并使用最新消息重新填表吗?')) { + try { + await TableManager.rollbackAndRefill(); + } catch (error) { + console.error('[内存储司] 回退重填功能失败:', error); + toastr.error('回退重填失败,请检查系统状态。'); + } + } + }); + + rollbackBtn.dataset.rollbackEventBound = '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 || !ruleSaveBtn || !flowSaveBtn) { + log('无法找到指令模板编辑器或其按钮,绑定失败。', 'warn'); + return; + } + + if (ruleSaveBtn.dataset.templateEventsBound) { + 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'); + } + }); + + ruleSaveBtn.dataset.templateEventsBound = 'true'; + flowSaveBtn.dataset.templateEventsBound = 'true'; + 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) { + const saveUrl = () => { + settings.nccsApiUrl = urlInput.value; + saveSettingsDebounced(); + }; + + urlInput.addEventListener('blur', saveUrl); + } + + if (keyInput) { + const saveKey = () => { + settings.nccsApiKey = keyInput.value; + saveSettingsDebounced(); + }; + + keyInput.addEventListener('blur', saveKey); + } + + if (modelInput) { + const saveModel = () => { + settings.nccsModel = modelInput.value; + saveSettingsDebounced(); + }; + + modelInput.addEventListener('blur', saveModel); + modelInput.addEventListener('input', saveModel); + } + + 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 = ' 测试中...'; + + 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 = ' 测试连接'; + } + }); + } + + if (fetchModelsButton) { + fetchModelsButton.addEventListener('click', async () => { + fetchModelsButton.disabled = true; + fetchModelsButton.innerHTML = ' 获取中...'; + + if (urlInput) { + settings.nccsApiUrl = urlInput.value; + } + if (keyInput) { + settings.nccsApiKey = keyInput.value; + } + saveSettingsDebounced(); + + try { + const models = await fetchNccsModels(); + if (models && models.length > 0) { + let modelSelect = document.getElementById('nccs-api-model-select'); + if (!modelSelect) { + modelSelect = document.createElement('select'); + modelSelect.id = 'nccs-api-model-select'; + modelSelect.className = 'text_pole'; + modelInput.parentNode.insertBefore(modelSelect, modelInput.nextSibling); + } + + modelSelect.innerHTML = ''; + models.forEach(model => { + const option = document.createElement('option'); + option.value = model.id || model.name; + option.textContent = model.name || model.id; + if ((model.id || model.name) === settings.nccsModel) { + option.selected = true; + } + modelSelect.appendChild(option); + }); + + modelInput.style.display = 'none'; + modelSelect.style.display = 'block'; + + modelSelect.addEventListener('change', () => { + const selectedModel = modelSelect.value; + settings.nccsModel = selectedModel; + modelInput.value = selectedModel; + saveSettingsDebounced(); + }); + + 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 = ' 获取模型'; + } + }); + } + + 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; + + const currentProfileId = settings.nccsTavernProfile; + + presetSelect.innerHTML = ''; + presetSelect.appendChild(new Option('选择预设', '', false, false)); + + if (profiles && profiles.length > 0) { + profiles.forEach(profile => { + const isSelected = profile.id === currentProfileId; + const option = new Option(profile.name, profile.id, isSelected, isSelected); + 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'); +} + +function bindChatTableDisplaySetting() { + const settings = extension_settings[extensionName]; + const showInChatToggle = document.getElementById('show-table-in-chat-toggle'); + const continuousRenderToggle = document.getElementById('render-on-every-message-toggle'); + + if (!showInChatToggle || !continuousRenderToggle) { + log('找不到聊天内表格相关的开关,绑定失败。', 'warn'); + return; + } + + showInChatToggle.checked = settings.show_table_in_chat === true; + continuousRenderToggle.checked = settings.render_on_every_message === true; + + const updateContinuousRenderState = () => { + if (showInChatToggle.checked) { + continuousRenderToggle.disabled = false; + continuousRenderToggle.closest('.control-block-with-switch').style.opacity = '1'; + } else { + continuousRenderToggle.disabled = true; + continuousRenderToggle.closest('.control-block-with-switch').style.opacity = '0.5'; + } + }; + + updateContinuousRenderState(); + + showInChatToggle.addEventListener('change', () => { + settings.show_table_in_chat = showInChatToggle.checked; + saveSettingsDebounced(); + toastr.info(`聊天内表格显示已${showInChatToggle.checked ? '开启' : '关闭'}。`); + updateContinuousRenderState(); + }); + + continuousRenderToggle.addEventListener('change', () => { + settings.render_on_every_message = continuousRenderToggle.checked; + saveSettingsDebounced(); + toastr.info(`持续渲染最新消息功能已${continuousRenderToggle.checked ? '开启' : '关闭'}。请切换聊天以应用更改。`); + }); + + log('聊天内表格显示设置及其依赖关系已成功绑定。', 'success'); +}