diff --git a/ui/table-bindings.js b/ui/table-bindings.js index ad0aff9..75bd1ba 100644 --- a/ui/table-bindings.js +++ b/ui/table-bindings.js @@ -1,2187 +1,654 @@ -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 { getMemoryState, getHighlights } from '../core/table-system/manager.js'; +import { extension_settings } from '/scripts/extensions.js'; import { extensionName } from '../utils/settings.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'; +import { getContext } from '/scripts/extensions.js'; +const TABLE_CONTAINER_ID = 'amily2-chat-table-container'; const isTouchDevice = () => window.matchMedia('(pointer: coarse)').matches; -const getAllTablesContainer = () => document.getElementById('all-tables-container'); -let isResizing = false; +// 【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; + } + /* 装饰性角落 */ + #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; + } -function toggleRowContextMenu(event) { - event.preventDefault(); - event.stopPropagation(); + /* 侧边栏:导航菜单 */ + .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; + } - const targetTd = event.target.closest('td.index-col'); - if (!targetTd) return; + .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 tableWrapper = targetTd.closest('.amily2-table-wrapper'); - if (!tableWrapper) return; + .amily2-game-tab i { + width: 24px; + text-align: center; + margin-right: 8px; + } - 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-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); } } - }); - targetTd.classList.toggle('amily2-menu-open'); + /* 折叠功能样式 */ + #amily2-chat-table-container.collapsed { + min-height: 30px !important; + height: 30px !important; + resize: none !important; + overflow: hidden !important; + border-bottom: none; + } - 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 = ''; + #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 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); - } - }; + // 过滤掉空表格 + const activeTables = tables.map((t, i) => ({...t, originalIndex: i})).filter(t => t.rows && t.rows.length > 0); + if (activeTables.length === 0) return ''; - if (targetTd.classList.contains('amily2-menu-open')) { - setTimeout(() => { - document.addEventListener('click', closeMenu, true); - }, 0); + // 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 += '
'; + + return `
${toggleHtml}${sidebarHtml}${contentHtml}
`; +} + +function removeTableContainer() { + const existingContainer = document.getElementById(TABLE_CONTAINER_ID); + if (existingContainer) { + existingContainer.remove(); } } - -function toggleColumnContextMenu(event) { - if (isResizing || event.target.classList.contains('amily2-resizer')) { +function bindSwipePreventer(container) { + if (!isTouchDevice()) { return; } - event.preventDefault(); - event.stopPropagation(); - const targetTh = event.target.closest('th'); - if (!targetTh) return; + let touchstartX = 0; + let touchstartY = 0; - const tableWrapper = targetTh.closest('.amily2-table-wrapper'); - if (!tableWrapper) return; + container.addEventListener('touchstart', function(event) { + touchstartX = event.changedTouches[0].screenX; + touchstartY = event.changedTouches[0].screenY; + }, { passive: true }); - const isActive = targetTh.classList.contains('amily2-menu-open'); + container.addEventListener('touchmove', function(event) { + const touchendX = event.changedTouches[0].screenX; + const touchendY = event.changedTouches[0].screenY; - 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 = ''; - } + const deltaX = Math.abs(touchendX - touchstartX); + const deltaY = Math.abs(touchendY - touchstartY); + + if (deltaX > deltaY) { + event.stopPropagation(); } - }); - - 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); - } + }, { passive: false }); } - -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); - } - }; +export function updateOrInsertTableInChat() { + injectChatTableStyles(); // 确保样式已注入 setTimeout(() => { - if (menu.classList.contains('amily2-menu-active')) { - document.addEventListener('click', closeMenu, true); + 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] 未找到最后一条消息的容器,无法插入表格。'); } }, 0); } - -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(); +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); }; - - 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); -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('[内存储司-工部] 缺少表格数据或容器,无法渲染。'); +export function startContinuousRendering() { + if (chatObserver) { + console.log('[Amily2] Continuous rendering is already active.'); return; } - const highlights = TableManager.getHighlights(); - const updatedTables = TableManager.getUpdatedTables(); - const fragment = document.createDocumentFragment(); - - const placeholder = document.getElementById('add-table-placeholder'); - if (placeholder) { - placeholder.remove(); + const chatContainer = document.getElementById('chat'); + if (!chatContainer) { + console.error('[Amily2] Could not find chat container to observe.'); + setTimeout(startContinuousRendering, 500); + return; } - tables.forEach((tableData, 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 observerConfig = { childList: true }; - const moveUpBtn = tableIndex > 0 ? `` : ''; - const moveDownBtn = tableIndex < tables.length - 1 ? `` : ''; - - controls.innerHTML = ` - ${moveUpBtn} - ${moveDownBtn} - - - `; - 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'); + 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; + } + } } - tableWrapper.appendChild(tableElement); - fragment.appendChild(tableWrapper); }); - container.innerHTML = ''; - container.appendChild(fragment); - - if (placeholder) { - container.appendChild(placeholder); - } - + chatObserver.observe(chatContainer, observerConfig); + console.log('[Amily2] Started continuous table rendering.'); updateOrInsertTableInChat(); } - -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(); - } - }); - } - - - 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')) { - 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); - 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'); +export function stopContinuousRendering() { + if (chatObserver) { + chatObserver.disconnect(); + chatObserver = null; + removeTableContainer(); + console.log('[Amily2] Stopped continuous table rendering.'); } } - -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'); -}