From b5a1d401eacd1fd834c6805f2ce5249f1333ac61 Mon Sep 17 00:00:00 2001 From: Wx-2025 <351320169@qq.com> Date: Sun, 23 Nov 2025 22:14:53 +0800 Subject: [PATCH] Update cwb_uiManager.js --- CharacterWorldBook/src/cwb_uiManager.js | 1356 ++++++++++++----------- 1 file changed, 702 insertions(+), 654 deletions(-) diff --git a/CharacterWorldBook/src/cwb_uiManager.js b/CharacterWorldBook/src/cwb_uiManager.js index 450cf7b..9447dc4 100644 --- a/CharacterWorldBook/src/cwb_uiManager.js +++ b/CharacterWorldBook/src/cwb_uiManager.js @@ -1,692 +1,740 @@ -import { SCRIPT_ID_PREFIX, CHAR_CARD_VIEWER_BUTTON_ID, CHAR_CARD_VIEWER_POPUP_ID, state } from './cwb_state.js'; -import { logDebug, logError, showToastr, escapeHtml, parseCustomFormat, buildCustomFormat, isCwbEnabled } from './cwb_utils.js'; -import { deleteLorebookEntries, getTargetWorldBook } from './cwb_lorebookManager.js'; -import { manualUpdateLogic } from './cwb_core.js'; -import { testCwbConnection, fetchCwbModels } from './cwb_apiService.js'; -import { extensionName } from '../../utils/settings.js'; -import { extension_settings } from '/scripts/extensions.js'; -import { saveSettingsDebounced } from '/script.js'; -import { amilyHelper } from '../../core/tavern-helper/main.js'; + import { SCRIPT_ID_PREFIX, CHAR_CARD_VIEWER_BUTTON_ID, CHAR_CARD_VIEWER_POPUP_ID, state } from './cwb_state.js'; + import { logDebug, logError, showToastr, escapeHtml, parseCustomFormat, buildCustomFormat, isCwbEnabled } from './cwb_utils.js'; + import { deleteLorebookEntries, getTargetWorldBook } from './cwb_lorebookManager.js'; + import { manualUpdateLogic } from './cwb_core.js'; + import { testCwbConnection, fetchCwbModels } from './cwb_apiService.js'; + import { extensionName } from '../../utils/settings.js'; + import { extension_settings } from '/scripts/extensions.js'; + import { saveSettingsDebounced } from '/script.js'; + import { amilyHelper } from '../../core/tavern-helper/main.js'; -const { jQuery: $, SillyTavern } = window; + const { jQuery: $, SillyTavern } = window; -function createCharCardViewerPopupHtml(displayItems) { - const pathToLabelMap = { - 'narrative_essence.core_traits.name': '特质名称', - 'narrative_essence.key_relationships.name': '关系人姓名', - }; - const keyToLabelMap = { - 'name': '姓名', - 'archetype': '身份原型', - 'gender': '性别', - 'age': '年龄', - 'race': '种族', - 'current_status': '当前状态', + function createCharCardViewerPopupHtml(displayItems) { + const pathToLabelMap = { + 'narrative_essence.core_traits.name': '特质名称', + 'narrative_essence.key_relationships.name': '关系人姓名', + 'NE.trait.name': '特质名称', + 'NE.rel.name': '关系人姓名', + }; + const keyToLabelMap = { + 'name': '姓名', + // Old keys + 'archetype': '身份原型', + 'gender': '性别', + 'age': '年龄', + 'race': '种族', + 'current_status': '当前状态', + 'first_impression': '第一印象', + 'key_features': '显著特征', + 'attire': '衣着风格', + 'mannerisms': '习惯举止', + 'voice': '声音特征', + 'tags': '性格标签', + 'description': '性格详述', + 'motivation': '内在驱动', + 'values': '价值观', + 'inner_conflict': '内心挣扎', + 'interaction_style': '互动风格', + 'skills': '技能能力', + 'reputation': '他人声望', + 'core_traits': '核心特质', + 'verbal_patterns': '语言范式', + 'key_relationships': '关键关系', + 'definition': '特质定义', + 'evidence': '具体事例', + 'style_summary': '风格总结', + 'quotes': '代表性引言', + 'summary': '关系概述', - 'first_impression': '第一印象', - 'key_features': '显著特征', - 'attire': '衣着风格', - 'mannerisms': '习惯举止', - 'voice': '声音特征', + // New short keys + 'CI': '核心认同', + 'PI': '物理印记', + 'PP': '心智侧写', + 'SM': '社交矩阵', + 'NE': '叙事精粹', + + 'arch': '身份原型', + 'gen': '性别', + // age is same + // race is same + 'status': '当前状态', - 'tags': '性格标签', - 'description': '性格详述', - 'motivation': '内在驱动', - 'values': '价值观', - 'inner_conflict': '内心挣扎', + 'first': '第一印象', + 'feat': '显著特征', + // attire is same + 'manner': '习惯举止', + // voice is same - 'interaction_style': '互动风格', - 'skills': '技能能力', - 'reputation': '他人声望', + // tags is same + 'desc': '性格详述', + 'mot': '内在驱动', + 'val': '价值观', + 'conf': '内心挣扎', - 'core_traits': '核心特质', - 'verbal_patterns': '语言范式', - 'key_relationships': '关键关系', - 'definition': '特质定义', - 'evidence': '具体事例', - 'style_summary': '风格总结', - 'quotes': '代表性引言', - 'summary': '关系概述', - }; - const getLabel = (key, path) => { - const pathKey = path.replace(/\.\d+\./g, '.'); - if (pathToLabelMap[pathKey]) { - return pathToLabelMap[pathKey]; - } - return keyToLabelMap[key] || key.replace(/_/g, ' '); - }; + 'style': '互动风格/风格总结', // Shared by SM.style and NE.verb.style + 'skill': '技能能力', + 'rep': '他人声望', - const renderField = (label, path, value, isTextarea = false, isArray = false) => { - const escapedLabel = escapeHtml(label); - const escapedValue = escapeHtml(isArray ? value.join('\n') : value || ''); + 'trait': '核心特质', + 'verb': '语言范式', + 'rel': '关键关系', - const isLongContent = (value && String(value).length > 50) || (Array.isArray(value) && value.length > 1); - const rows = isArray ? Math.max(3, value.length) : (isLongContent ? 4 : 2); - - const inputElement = ``; - - return `
- - ${inputElement} -
`; - }; - - const renderCard = (title, data, pathPrefix) => { - if (!data || typeof data !== 'object' || Object.keys(data).length === 0) return ''; - let cardHtml = `

${escapeHtml(title)}

`; - for (const [key, value] of Object.entries(data)) { - const currentPath = pathPrefix ? `${pathPrefix}.${key}` : key; - const label = getLabel(key, currentPath); - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - cardHtml += renderCard(label, value, currentPath); // Recursive call for nested objects - } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') { - cardHtml += `
${escapeHtml(label)}
`; - value.forEach((item, itemIndex) => { - cardHtml += `
`; - for (const [itemKey, itemValue] of Object.entries(item)) { - const itemPath = `${currentPath}.${itemIndex}.${itemKey}`; - cardHtml += renderField(getLabel(itemKey, itemPath), itemPath, itemValue, false, Array.isArray(itemValue)); - } - cardHtml += `
`; - }); - cardHtml += `
`; - } else { - cardHtml += renderField(label, currentPath, value, false, Array.isArray(value)); + 'def': '特质定义', + 'evid': '具体事例', + 'quote': '代表性引言', + 'sum': '关系概述', + }; + const getLabel = (key, path) => { + const pathKey = path.replace(/\.\d+\./g, '.'); + if (pathToLabelMap[pathKey]) { + return pathToLabelMap[pathKey]; } + return keyToLabelMap[key] || key.replace(/_/g, ' '); + }; + + const renderField = (label, path, value, isTextarea = false, isArray = false) => { + const escapedLabel = escapeHtml(label); + const escapedValue = escapeHtml(isArray ? value.join('\n') : value || ''); + + const isLongContent = (value && String(value).length > 50) || (Array.isArray(value) && value.length > 1); + const rows = isArray ? Math.max(3, value.length) : (isLongContent ? 4 : 2); + + const inputElement = ``; + + return `
+ + ${inputElement} +
`; + }; + + const renderCard = (title, data, pathPrefix) => { + if (!data || typeof data !== 'object' || Object.keys(data).length === 0) return ''; + let cardHtml = `

${escapeHtml(title)}

`; + for (const [key, value] of Object.entries(data)) { + const currentPath = pathPrefix ? `${pathPrefix}.${key}` : key; + const label = getLabel(key, currentPath); + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + cardHtml += renderCard(label, value, currentPath); // Recursive call for nested objects + } else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') { + cardHtml += `
${escapeHtml(label)}
`; + value.forEach((item, itemIndex) => { + cardHtml += `
`; + for (const [itemKey, itemValue] of Object.entries(item)) { + const itemPath = `${currentPath}.${itemIndex}.${itemKey}`; + cardHtml += renderField(getLabel(itemKey, itemPath), itemPath, itemValue, false, Array.isArray(itemValue)); + } + cardHtml += `
`; + }); + cardHtml += `
`; + } else { + cardHtml += renderField(label, currentPath, value, false, Array.isArray(value)); + } + } + cardHtml += `
`; + return cardHtml; + }; + + let html = `
`; + html += `
+

角色数据核心

+
+ + + + +
+
`; + + if (!displayItems || displayItems.length === 0) { + html += `

看什么?没更新角色条目就等着我给你显示出来条目吗?想关悬浮窗就点角色世界,功能设置关掉。

`; + return html; } - cardHtml += `
`; - return cardHtml; - }; - let html = `
`; - html += `
-

角色数据核心

-
- - - - -
-
`; + html += `
`; + html += `
`; + displayItems.forEach((item, index) => { + const itemName = item.isRoster ? '人物总览' : (item.parsed?.name || `未知实体 ${index + 1}`); + const wrapperClass = index === 0 ? 'cwb-cyber-tab active' : 'cwb-cyber-tab'; + html += `
+ + +
`; + }); + html += `
`; - if (!displayItems || displayItems.length === 0) { - html += `

看什么?没更新角色条目就等着我给你显示出来条目吗?想关悬浮窗就点角色世界,功能设置关掉。

`; + html += `
`; + displayItems.forEach((item, index) => { + html += `
`; + if (item.isRoster) { + html += `
+

人物总览 (只读)

+
+ +
+
`; + } else { + const charData = item.parsed; + if (charData) { + const charName = charData.name || `角色 ${index + 1}`; + if (charData.name) html += renderCard('姓名', { name: charData.name }, ''); + + // Support both old and new formats + if (charData.core_identity) html += renderCard('核心认同', charData.core_identity, 'core_identity'); + if (charData.CI) html += renderCard('核心认同', charData.CI, 'CI'); + + if (charData.physical_imprint) html += renderCard('物理印记', charData.physical_imprint, 'physical_imprint'); + if (charData.PI) html += renderCard('物理印记', charData.PI, 'PI'); + + if (charData.psyche_profile) html += renderCard('心智侧写', charData.psyche_profile, 'psyche_profile'); + if (charData.PP) html += renderCard('心智侧写', charData.PP, 'PP'); + + if (charData.social_matrix) html += renderCard('社交矩阵', charData.social_matrix, 'social_matrix'); + if (charData.SM) html += renderCard('社交矩阵', charData.SM, 'SM'); + + if (charData.narrative_essence) html += renderCard('叙事精粹', charData.narrative_essence, 'narrative_essence'); + if (charData.NE) html += renderCard('叙事精粹', charData.NE, 'NE'); + + html += `
+

注入设置

+
+
+ + +
+
+ + +
+
+ + +
+
+
`; + + html += ``; + } + } + html += `
`; + }); + html += `
`; return html; } - html += `
`; - html += `
`; - displayItems.forEach((item, index) => { - const itemName = item.isRoster ? '人物总览' : (item.parsed?.name || `未知实体 ${index + 1}`); - const wrapperClass = index === 0 ? 'cwb-cyber-tab active' : 'cwb-cyber-tab'; - html += `
- - -
`; - }); - html += `
`; - - html += `
`; - displayItems.forEach((item, index) => { - html += `
`; - if (item.isRoster) { - html += `
-

人物总览 (只读)

-
- -
-
`; - } else { - const charData = item.parsed; - if (charData) { - const charName = charData.name || `角色 ${index + 1}`; - if (charData.name) html += renderCard('姓名', { name: charData.name }, ''); - if (charData.core_identity) html += renderCard('核心认同', charData.core_identity, 'core_identity'); - if (charData.physical_imprint) html += renderCard('物理印记', charData.physical_imprint, 'physical_imprint'); - if (charData.psyche_profile) html += renderCard('心智侧写', charData.psyche_profile, 'psyche_profile'); - if (charData.social_matrix) html += renderCard('社交矩阵', charData.social_matrix, 'social_matrix'); - if (charData.narrative_essence) html += renderCard('叙事精粹', charData.narrative_essence, 'narrative_essence'); - - html += `
-

注入设置

-
-
- - -
-
- - -
-
- - -
-
-
`; - - html += ``; - } - } - html += `
`; - }); - html += `
`; - return html; -} - -function bindCharCardViewerPopupEvents($popup) { - $popup.on('change', '.cwb-insertion-position', function() { - const $this = $(this); - const $depthContainer = $this.closest('.cwb-insertion-settings-content').find('.cwb-insertion-depth-container'); - if ($this.val() === 'at_depth') { - $depthContainer.show(); - } else { - $depthContainer.hide(); - } - }); - - $popup.on('click', '.cwb-viewer-popup-close-button', closeCharCardViewerPopup); - $popup.find('#cwb-viewer-refresh').on('click', () => { - showToastr('info', '正在刷新角色数据...'); - showCharCardViewerPopup(); - }); - - $popup.find('#cwb-manual-update-btn').on('click', async function() { - const $button = $(this); - $button.prop('disabled', true).html(' 更新中...'); - await manualUpdateLogic(); - showToastr('info', '更新完成,正在刷新查看器...'); - showCharCardViewerPopup(); - }); - - $popup.find('.cwb-cyber-tab__button').on('click', function () { - const $this = $(this); - const targetUid = $this.data('char-uid'); - $popup.find('.cwb-cyber-tab').removeClass('active'); - $this.closest('.cwb-cyber-tab').addClass('active'); - $popup.find('.cwb-cyber-content-pane').removeClass('active'); - $popup.find(`#cwb-char-content-${targetUid}`).addClass('active'); - }); - - $popup.find('.cwb-cyber-tab__delete').on('click', async function(e) { - e.stopPropagation(); - if (confirm('您确定要删除这个角色条目吗?此操作不可撤销。')) { - const uidToDelete = $(this).data('char-uid'); - await deleteLorebookEntries([uidToDelete]); - const $wrapper = $(this).closest('.cwb-cyber-tab'); - const $pane = $popup.find(`#cwb-char-content-${uidToDelete}`); - const wasActive = $wrapper.hasClass('active'); - $wrapper.remove(); - $pane.remove(); - if (wasActive && $popup.find('.cwb-cyber-tab').length > 0) { - $popup.find('.cwb-cyber-tab').first().find('.cwb-cyber-tab__button').trigger('click'); - } else if ($popup.find('.cwb-cyber-tab').length === 0) { - showCharCardViewerPopup(); - } - } - }); - - $popup.find('#cwb-viewer-delete-all').on('click', async function() { - if (confirm('您确定要清除当前聊天中的所有角色卡和总览吗?此操作将删除所有相关条目,且不可撤销。')) { - const allUids = $popup.find('.cwb-cyber-tab__button').map(function() { - return $(this).data('char-uid'); - }).get(); - if (allUids.length > 0) { - await deleteLorebookEntries(allUids); - } - showCharCardViewerPopup(); - } - }); - - $popup.find('.cwb-save-button').on('click', async function () { - const $button = $(this); - const targetUid = $button.data('uid'); - $button.prop('disabled', true).html(' 保存中...'); - try { - const book = await getTargetWorldBook(); - if (!book) throw new Error('未找到目标世界书。'); - const $activePane = $popup.find(`#cwb-char-content-${targetUid}`); - const collectedData = {}; - const setNestedValue = (obj, path, value) => { - const keys = path.split('.'); - let current = obj; - keys.forEach((key, index) => { - if (index === keys.length - 1) { - current[key] = value === '' ? null : value; - } else { - const nextKeyIsNumber = /^\d+$/.test(keys[index + 1]); - if (!current[key]) { - current[key] = nextKeyIsNumber ? [] : {}; - } - current = current[key]; - } - }); - }; - $activePane.find('.cwb-cyber-field__input').each(function () { - const $field = $(this); - const path = $field.data('path'); - let value = $field.val(); - if ($field.data('is-array')) { - value = value.split('\n').map(l => l.trim()).filter(Boolean); - } - if(path){ - setNestedValue(collectedData, path, value); - } - }); - const finalContentToSave = buildCustomFormat(collectedData); - - const insertionPosition = $activePane.find('.cwb-insertion-position').val(); - const insertionDepth = parseInt($activePane.find('.cwb-insertion-depth').val(), 10); - const insertionOrder = parseInt($activePane.find('.cwb-insertion-order').val(), 10); - - logDebug(`[DEBUG] 界面收集值 UID:${targetUid}`, { - insertionPosition: insertionPosition, - insertionDepth: insertionDepth, - insertionOrder: insertionOrder - }); - - const positionMap = { - 'before_char': 'before_character_definition', - 'after_char': 'after_character_definition', - 'before_an': 'before_author_note', - 'after_an': 'after_author_note', - 'at_depth': 'at_depth_as_system' - }; - - const finalEntryData = { - uid: targetUid, - content: finalContentToSave, - position: positionMap[insertionPosition] || 'before_character_definition', - order: isNaN(insertionOrder) ? 7001 : insertionOrder, - }; - - if (insertionPosition === 'at_depth') { - finalEntryData.depth = isNaN(insertionDepth) ? 0 : insertionDepth; + function bindCharCardViewerPopupEvents($popup) { + $popup.on('change', '.cwb-insertion-position', function() { + const $this = $(this); + const $depthContainer = $this.closest('.cwb-insertion-settings-content').find('.cwb-insertion-depth-container'); + if ($this.val() === 'at_depth') { + $depthContainer.show(); } else { - finalEntryData.depth = null; + $depthContainer.hide(); } - - logDebug(`[DEBUG] 最终保存数据 UID:${targetUid}`, { - position: finalEntryData.position, - depth: finalEntryData.depth, - order: finalEntryData.order, - hasDepthField: 'depth' in finalEntryData - }); - - await amilyHelper.setLorebookEntries(book, [finalEntryData]); - showToastr('success', '角色卡已成功保存!'); - } catch (error) { - logError('保存角色卡失败:', error); - showToastr('error', `保存失败: ${error.message}`); - } finally { - $button.prop('disabled', false).text(`保存修改`); - } - }); -} - -function closeCharCardViewerPopup() { - $(`#${CHAR_CARD_VIEWER_POPUP_ID}`).remove(); -} - -export async function showCharCardViewerPopup() { - if (!isCwbEnabled()) return; - closeCharCardViewerPopup(); - try { - const book = await getTargetWorldBook(); - if (!book) { - showToastr('warning', '当前角色未设置主世界书或自定义世界书。'); - $('body').append(createCharCardViewerPopupHtml([])); - bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`)); - return; - } - const allEntries = await amilyHelper.getLorebookEntries(book); - let currentChatId = state.currentChatFileIdentifier; - - if (!currentChatId || currentChatId.startsWith('unknown_chat')) { - logError(`Invalid chat identifier "${currentChatId}" for viewer.`); - $('body').append(createCharCardViewerPopupHtml([])); - bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`)); - return; - } - - const cleanChatId = currentChatId.replace(/ imported/g, ''); - let displayItems = []; - - let relevantEntries; - if (state.worldbookTarget === 'custom' && state.customWorldBook) { - relevantEntries = allEntries.filter(entry => { - if (!entry.enabled || !Array.isArray(entry.keys)) return false; - if (entry.keys.includes('Amily2角色总集') || entry.keys.includes('角色总览')) return true; - if (entry.content) { - try { - const parsed = parseCustomFormat(entry.content); - return parsed && Object.keys(parsed).length > 0; - } catch (e) { - return false; - } - } - - return false; - }); - } else { - relevantEntries = allEntries.filter(entry => - entry.enabled && - Array.isArray(entry.keys) && - entry.keys.includes(cleanChatId) - ); - } - - const rosterEntries = relevantEntries.filter(entry => - entry.keys.includes('Amily2角色总集') && entry.keys.includes('角色总览') - ); - - rosterEntries.forEach((entry, index) => { - displayItems.push({ - uid: entry.uid, - isRoster: true, - comment: entry.comment, - content: entry.content, - rosterIndex: index - }); }); - const characterEntries = relevantEntries - .filter(entry => !entry.keys.includes('Amily2角色总集')) - .map(entry => { - try { - logDebug(`[DEBUG] 原始条目数据 UID:${entry.uid}`, { - position: entry.position, - depth: entry.depth, - order: entry.order, - comment: entry.comment - }); + $popup.on('click', '.cwb-viewer-popup-close-button', closeCharCardViewerPopup); + $popup.find('#cwb-viewer-refresh').on('click', () => { + showToastr('info', '正在刷新角色数据...'); + showCharCardViewerPopup(); + }); - const positionStringMap = { - 0: 'before_char', - 1: 'after_char', - 2: 'before_an', - 3: 'after_an', - 4: 'at_depth', - 'before_character_definition': 'before_char', - 'after_character_definition': 'after_char', - 'before_author_note': 'before_an', - 'after_author_note': 'after_an', - 'at_depth_as_system': 'at_depth' - }; + $popup.find('#cwb-manual-update-btn').on('click', async function() { + const $button = $(this); + $button.prop('disabled', true).html(' 更新中...'); + await manualUpdateLogic(); + showToastr('info', '更新完成,正在刷新查看器...'); + showCharCardViewerPopup(); + }); - const position = entry.position; - const mappedPosition = positionStringMap[position] || 'at_depth'; - const finalDepth = (position === 4 || position === 'at_depth_as_system') ? (entry.depth ?? 0) : 0; - logDebug(`[DEBUG] 映射结果 UID:${entry.uid}`, { - originalPosition: position, - mappedPosition: mappedPosition, - finalDepth: finalDepth - }); + $popup.find('.cwb-cyber-tab__button').on('click', function () { + const $this = $(this); + const targetUid = $this.data('char-uid'); + $popup.find('.cwb-cyber-tab').removeClass('active'); + $this.closest('.cwb-cyber-tab').addClass('active'); + $popup.find('.cwb-cyber-content-pane').removeClass('active'); + $popup.find(`#cwb-char-content-${targetUid}`).addClass('active'); + }); - return { - uid: entry.uid, - isRoster: false, - comment: entry.comment, - content: entry.content, - parsed: parseCustomFormat(entry.content), - insertionPosition: mappedPosition, - insertionDepth: finalDepth, - insertionOrder: entry.order ?? 7001, - }; - } catch (e) { - logError(`解析角色条目失败 (UID: ${entry.uid}),已跳过。`, e); - return null; - } - }) - .filter(c => c && c.parsed && Object.keys(c.parsed).length > 0); - - displayItems = displayItems.concat(characterEntries); - - const popupHtml = createCharCardViewerPopupHtml(displayItems); - $('body').append(popupHtml); - const $popup = $(`#${CHAR_CARD_VIEWER_POPUP_ID}`); - bindCharCardViewerPopupEvents($popup); - } catch (error) { - logError('无法显示角色卡查看器:', error); - showToastr('error', '加载角色卡数据时出错。'); - } -} - -function toggleCharCardViewerPopup() { - if ($(`#${CHAR_CARD_VIEWER_POPUP_ID}`).length > 0) { - closeCharCardViewerPopup(); - } else { - showCharCardViewerPopup(); - } -} - -function keepButtonInBounds($element, savePosition = false) { - if (!$element || !$element.length) return; - const windowWidth = $(window).width(); - const windowHeight = $(window).height(); - const buttonWidth = $element.outerWidth(); - const buttonHeight = $element.outerHeight(); - let currentPos = $element.offset(); - let newTop = Math.max(0, Math.min(currentPos.top, windowHeight - buttonHeight)); - let newLeft = Math.max(0, Math.min(currentPos.left, windowWidth - buttonWidth)); - $element.css({ top: `${newTop}px`, left: `${newLeft}px` }); - if (savePosition) { - localStorage.setItem(state.STORAGE_KEY_VIEWER_BUTTON_POS, JSON.stringify({ top: $element.css('top'), left: $element.css('left') })); - } -} - -function makeButtonDraggable($button) { - let isDragging = false, wasDragged = false, offset = { x: 0, y: 0 }, startPos = { x: 0, y: 0 }; - const DRAG_THRESHOLD = 5; // 5 pixels threshold - - const getCoords = (e) => e.touches && e.touches.length ? e.touches[0] : e; - - const dragStart = function (e) { - if (e.type === 'touchstart') e.preventDefault(); - isDragging = true; - wasDragged = false; - const coords = getCoords(e); - startPos.x = coords.clientX; - startPos.y = coords.clientY; - offset.x = coords.clientX - $button.offset().left; - offset.y = coords.clientY - $button.offset().top; - $button.css('cursor', 'grabbing'); - $('body').css({ 'user-select': 'none', '-webkit-user-select': 'none' }); - }; - - const dragMove = function (e) { - if (!isDragging) return; - const coords = getCoords(e); - const dx = coords.clientX - startPos.x; - const dy = coords.clientY - startPos.y; - - if (!wasDragged && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) { - wasDragged = true; - } - - if (wasDragged) { - if (e.type === 'touchmove') e.preventDefault(); - let newX = coords.clientX - offset.x; - let newY = coords.clientY - offset.y; - newX = Math.max(0, Math.min(newX, window.innerWidth - $button.outerWidth())); - newY = Math.max(0, Math.min(newY, window.innerHeight - $button.outerHeight())); - $button.css({ top: newY + 'px', left: newX + 'px', right: '', bottom: '' }); - } - }; - - const dragEnd = function (e) { - if (!isDragging) return; - isDragging = false; - $button.css('cursor', 'grab'); - $('body').css({ 'user-select': 'auto', '-webkit-user-select': 'auto' }); - if (wasDragged) { - keepButtonInBounds($button, true); - } else if (e.type === 'touchend') { - e.preventDefault(); - toggleCharCardViewerPopup(); - } - }; - - $button.on('mousedown', dragStart); - $(document).on('mousemove.cwbViewer', dragMove).on('mouseup.cwbViewer', dragEnd); - $button.on('touchstart', dragStart); - $(document).on('touchmove.cwbViewer', dragMove).on('touchend.cwbViewer', dragEnd); - - $button.on('click', function (e) { - if (wasDragged) { - e.preventDefault(); + $popup.find('.cwb-cyber-tab__delete').on('click', async function(e) { e.stopPropagation(); + if (confirm('您确定要删除这个角色条目吗?此操作不可撤销。')) { + const uidToDelete = $(this).data('char-uid'); + await deleteLorebookEntries([uidToDelete]); + const $wrapper = $(this).closest('.cwb-cyber-tab'); + const $pane = $popup.find(`#cwb-char-content-${uidToDelete}`); + const wasActive = $wrapper.hasClass('active'); + $wrapper.remove(); + $pane.remove(); + if (wasActive && $popup.find('.cwb-cyber-tab').length > 0) { + $popup.find('.cwb-cyber-tab').first().find('.cwb-cyber-tab__button').trigger('click'); + } else if ($popup.find('.cwb-cyber-tab').length === 0) { + showCharCardViewerPopup(); + } + } + }); + + $popup.find('#cwb-viewer-delete-all').on('click', async function() { + if (confirm('您确定要清除当前聊天中的所有角色卡和总览吗?此操作将删除所有相关条目,且不可撤销。')) { + const allUids = $popup.find('.cwb-cyber-tab__button').map(function() { + return $(this).data('char-uid'); + }).get(); + if (allUids.length > 0) { + await deleteLorebookEntries(allUids); + } + showCharCardViewerPopup(); + } + }); + + $popup.find('.cwb-save-button').on('click', async function () { + const $button = $(this); + const targetUid = $button.data('uid'); + $button.prop('disabled', true).html(' 保存中...'); + try { + const book = await getTargetWorldBook(); + if (!book) throw new Error('未找到目标世界书。'); + const $activePane = $popup.find(`#cwb-char-content-${targetUid}`); + const collectedData = {}; + const setNestedValue = (obj, path, value) => { + const keys = path.split('.'); + let current = obj; + keys.forEach((key, index) => { + if (index === keys.length - 1) { + current[key] = value === '' ? null : value; + } else { + const nextKeyIsNumber = /^\d+$/.test(keys[index + 1]); + if (!current[key]) { + current[key] = nextKeyIsNumber ? [] : {}; + } + current = current[key]; + } + }); + }; + $activePane.find('.cwb-cyber-field__input').each(function () { + const $field = $(this); + const path = $field.data('path'); + let value = $field.val(); + if ($field.data('is-array')) { + value = value.split('\n').map(l => l.trim()).filter(Boolean); + } + if(path){ + setNestedValue(collectedData, path, value); + } + }); + const finalContentToSave = buildCustomFormat(collectedData); + + const insertionPosition = $activePane.find('.cwb-insertion-position').val(); + const insertionDepth = parseInt($activePane.find('.cwb-insertion-depth').val(), 10); + const insertionOrder = parseInt($activePane.find('.cwb-insertion-order').val(), 10); + + logDebug(`[DEBUG] 界面收集值 UID:${targetUid}`, { + insertionPosition: insertionPosition, + insertionDepth: insertionDepth, + insertionOrder: insertionOrder + }); + + const positionMap = { + 'before_char': 'before_character_definition', + 'after_char': 'after_character_definition', + 'before_an': 'before_author_note', + 'after_an': 'after_author_note', + 'at_depth': 'at_depth_as_system' + }; + + const finalEntryData = { + uid: targetUid, + content: finalContentToSave, + position: positionMap[insertionPosition] || 'before_character_definition', + order: isNaN(insertionOrder) ? 7001 : insertionOrder, + }; + + if (insertionPosition === 'at_depth') { + finalEntryData.depth = isNaN(insertionDepth) ? 0 : insertionDepth; + } else { + finalEntryData.depth = null; + } + + logDebug(`[DEBUG] 最终保存数据 UID:${targetUid}`, { + position: finalEntryData.position, + depth: finalEntryData.depth, + order: finalEntryData.order, + hasDepthField: 'depth' in finalEntryData + }); + + await amilyHelper.setLorebookEntries(book, [finalEntryData]); + showToastr('success', '角色卡已成功保存!'); + } catch (error) { + logError('保存角色卡失败:', error); + showToastr('error', `保存失败: ${error.message}`); + } finally { + $button.prop('disabled', false).text(`保存修改`); + } + }); + } + + function closeCharCardViewerPopup() { + $(`#${CHAR_CARD_VIEWER_POPUP_ID}`).remove(); + } + + export async function showCharCardViewerPopup() { + if (!isCwbEnabled()) return; + closeCharCardViewerPopup(); + try { + const book = await getTargetWorldBook(); + if (!book) { + showToastr('warning', '当前角色未设置主世界书或自定义世界书。'); + $('body').append(createCharCardViewerPopupHtml([])); + bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`)); + return; + } + const allEntries = await amilyHelper.getLorebookEntries(book); + let currentChatId = state.currentChatFileIdentifier; + + if (!currentChatId || currentChatId.startsWith('unknown_chat')) { + logError(`Invalid chat identifier "${currentChatId}" for viewer.`); + $('body').append(createCharCardViewerPopupHtml([])); + bindCharCardViewerPopupEvents($(`#${CHAR_CARD_VIEWER_POPUP_ID}`)); + return; + } + + const cleanChatId = currentChatId.replace(/ imported/g, ''); + let displayItems = []; + + let relevantEntries; + if (state.worldbookTarget === 'custom' && state.customWorldBook) { + relevantEntries = allEntries.filter(entry => { + if (!entry.enabled || !Array.isArray(entry.keys)) return false; + if (entry.keys.includes('Amily2角色总集') || entry.keys.includes('角色总览')) return true; + if (entry.content) { + try { + const parsed = parseCustomFormat(entry.content); + return parsed && Object.keys(parsed).length > 0; + } catch (e) { + return false; + } + } + + return false; + }); + } else { + relevantEntries = allEntries.filter(entry => + entry.enabled && + Array.isArray(entry.keys) && + entry.keys.includes(cleanChatId) + ); + } + + const rosterEntries = relevantEntries.filter(entry => + entry.keys.includes('Amily2角色总集') && entry.keys.includes('角色总览') + ); + + rosterEntries.forEach((entry, index) => { + displayItems.push({ + uid: entry.uid, + isRoster: true, + comment: entry.comment, + content: entry.content, + rosterIndex: index + }); + }); + + const characterEntries = relevantEntries + .filter(entry => !entry.keys.includes('Amily2角色总集')) + .map(entry => { + try { + logDebug(`[DEBUG] 原始条目数据 UID:${entry.uid}`, { + position: entry.position, + depth: entry.depth, + order: entry.order, + comment: entry.comment + }); + + const positionStringMap = { + 0: 'before_char', + 1: 'after_char', + 2: 'before_an', + 3: 'after_an', + 4: 'at_depth', + 'before_character_definition': 'before_char', + 'after_character_definition': 'after_char', + 'before_author_note': 'before_an', + 'after_author_note': 'after_an', + 'at_depth_as_system': 'at_depth' + }; + + const position = entry.position; + const mappedPosition = positionStringMap[position] || 'at_depth'; + const finalDepth = (position === 4 || position === 'at_depth_as_system') ? (entry.depth ?? 0) : 0; + logDebug(`[DEBUG] 映射结果 UID:${entry.uid}`, { + originalPosition: position, + mappedPosition: mappedPosition, + finalDepth: finalDepth + }); + + return { + uid: entry.uid, + isRoster: false, + comment: entry.comment, + content: entry.content, + parsed: parseCustomFormat(entry.content), + insertionPosition: mappedPosition, + insertionDepth: finalDepth, + insertionOrder: entry.order ?? 7001, + }; + } catch (e) { + logError(`解析角色条目失败 (UID: ${entry.uid}),已跳过。`, e); + return null; + } + }) + .filter(c => c && c.parsed && Object.keys(c.parsed).length > 0); + + displayItems = displayItems.concat(characterEntries); + + const popupHtml = createCharCardViewerPopupHtml(displayItems); + $('body').append(popupHtml); + const $popup = $(`#${CHAR_CARD_VIEWER_POPUP_ID}`); + bindCharCardViewerPopupEvents($popup); + } catch (error) { + logError('无法显示角色卡查看器:', error); + showToastr('error', '加载角色卡数据时出错。'); + } + } + + function toggleCharCardViewerPopup() { + if ($(`#${CHAR_CARD_VIEWER_POPUP_ID}`).length > 0) { + closeCharCardViewerPopup(); + } else { + showCharCardViewerPopup(); + } + } + + function keepButtonInBounds($element, savePosition = false) { + if (!$element || !$element.length) return; + const windowWidth = $(window).width(); + const windowHeight = $(window).height(); + const buttonWidth = $element.outerWidth(); + const buttonHeight = $element.outerHeight(); + let currentPos = $element.offset(); + let newTop = Math.max(0, Math.min(currentPos.top, windowHeight - buttonHeight)); + let newLeft = Math.max(0, Math.min(currentPos.left, windowWidth - buttonWidth)); + $element.css({ top: `${newTop}px`, left: `${newLeft}px` }); + if (savePosition) { + localStorage.setItem(state.STORAGE_KEY_VIEWER_BUTTON_POS, JSON.stringify({ top: $element.css('top'), left: $element.css('left') })); + } + } + + function makeButtonDraggable($button) { + let isDragging = false, wasDragged = false, offset = { x: 0, y: 0 }, startPos = { x: 0, y: 0 }; + const DRAG_THRESHOLD = 5; // 5 pixels threshold + + const getCoords = (e) => e.touches && e.touches.length ? e.touches[0] : e; + + const dragStart = function (e) { + if (e.type === 'touchstart') e.preventDefault(); + isDragging = true; + wasDragged = false; + const coords = getCoords(e); + startPos.x = coords.clientX; + startPos.y = coords.clientY; + offset.x = coords.clientX - $button.offset().left; + offset.y = coords.clientY - $button.offset().top; + $button.css('cursor', 'grabbing'); + $('body').css({ 'user-select': 'none', '-webkit-user-select': 'none' }); + }; + + const dragMove = function (e) { + if (!isDragging) return; + const coords = getCoords(e); + const dx = coords.clientX - startPos.x; + const dy = coords.clientY - startPos.y; + + if (!wasDragged && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) { + wasDragged = true; + } + + if (wasDragged) { + if (e.type === 'touchmove') e.preventDefault(); + let newX = coords.clientX - offset.x; + let newY = coords.clientY - offset.y; + newX = Math.max(0, Math.min(newX, window.innerWidth - $button.outerWidth())); + newY = Math.max(0, Math.min(newY, window.innerHeight - $button.outerHeight())); + $button.css({ top: newY + 'px', left: newX + 'px', right: '', bottom: '' }); + } + }; + + const dragEnd = function (e) { + if (!isDragging) return; + isDragging = false; + $button.css('cursor', 'grab'); + $('body').css({ 'user-select': 'auto', '-webkit-user-select': 'auto' }); + if (wasDragged) { + keepButtonInBounds($button, true); + } else if (e.type === 'touchend') { + e.preventDefault(); + toggleCharCardViewerPopup(); + } + }; + + $button.on('mousedown', dragStart); + $(document).on('mousemove.cwbViewer', dragMove).on('mouseup.cwbViewer', dragEnd); + $button.on('touchstart', dragStart); + $(document).on('touchmove.cwbViewer', dragMove).on('touchend.cwbViewer', dragEnd); + + $button.on('click', function (e) { + if (wasDragged) { + e.preventDefault(); + e.stopPropagation(); + return; + } + toggleCharCardViewerPopup(); + }); + } + + export function initializeCharCardViewer() { + const $existingButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); + + if ($existingButton.length > 0) { + console.log('[CWB] Char card viewer button already exists'); + setTimeout(() => { + const shouldShow = isCwbEnabled() && state.viewerEnabled; + $existingButton.toggle(shouldShow); + console.log(`[CWB] Force updated existing button visibility: ${shouldShow}`); + }, 100); return; } - toggleCharCardViewerPopup(); - }); -} + + const buttonHtml = `
`; + $('body').append(buttonHtml); + const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); + makeButtonDraggable($viewerButton); + + const savedPosition = JSON.parse(localStorage.getItem(state.STORAGE_KEY_VIEWER_BUTTON_POS) || 'null'); + if (savedPosition) { + $viewerButton.css({ top: savedPosition.top, left: savedPosition.left }); + } else { + $viewerButton.css({ top: '120px', right: '10px', left: 'auto' }); + } -export function initializeCharCardViewer() { - const $existingButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); - - if ($existingButton.length > 0) { - console.log('[CWB] Char card viewer button already exists'); setTimeout(() => { const shouldShow = isCwbEnabled() && state.viewerEnabled; - $existingButton.toggle(shouldShow); - console.log(`[CWB] Force updated existing button visibility: ${shouldShow}`); + $viewerButton.toggle(shouldShow); + console.log(`[CWB] New button created with visibility: ${shouldShow}`); }, 100); - return; - } - - const buttonHtml = `
`; - $('body').append(buttonHtml); - const $viewerButton = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); - makeButtonDraggable($viewerButton); - - const savedPosition = JSON.parse(localStorage.getItem(state.STORAGE_KEY_VIEWER_BUTTON_POS) || 'null'); - if (savedPosition) { - $viewerButton.css({ top: savedPosition.top, left: savedPosition.left }); - } else { - $viewerButton.css({ top: '120px', right: '10px', left: 'auto' }); + + console.log('[CWB] Char card viewer button initialized'); + + let resizeTimeout; + $(window).on('resize.cwbViewer', function () { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => keepButtonInBounds($(`#${CHAR_CARD_VIEWER_BUTTON_ID}`), true), 150); + }); } - setTimeout(() => { + export function updateViewerButtonVisibility() { + const $button = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); const shouldShow = isCwbEnabled() && state.viewerEnabled; - $viewerButton.toggle(shouldShow); - console.log(`[CWB] New button created with visibility: ${shouldShow}`); - }, 100); - - console.log('[CWB] Char card viewer button initialized'); - - let resizeTimeout; - $(window).on('resize.cwbViewer', function () { - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(() => keepButtonInBounds($(`#${CHAR_CARD_VIEWER_BUTTON_ID}`), true), 150); - }); -} - -export function updateViewerButtonVisibility() { - const $button = $(`#${CHAR_CARD_VIEWER_BUTTON_ID}`); - const shouldShow = isCwbEnabled() && state.viewerEnabled; - - console.log(`[CWB] Updating viewer button visibility: ${shouldShow} (master: ${isCwbEnabled()}, viewer: ${state.viewerEnabled})`); - - if ($button.length > 0) { - $button.toggle(shouldShow); - console.log(`[CWB] Viewer button visibility set to: ${shouldShow}`); - } else { - console.log('[CWB] Viewer button not found, will initialize when DOM is ready'); - // Try to initialize if button doesn't exist yet - setTimeout(() => { - initializeCharCardViewer(); - }, 500); + + console.log(`[CWB] Updating viewer button visibility: ${shouldShow} (master: ${isCwbEnabled()}, viewer: ${state.viewerEnabled})`); + + if ($button.length > 0) { + $button.toggle(shouldShow); + console.log(`[CWB] Viewer button visibility set to: ${shouldShow}`); + } else { + console.log('[CWB] Viewer button not found, will initialize when DOM is ready'); + // Try to initialize if button doesn't exist yet + setTimeout(() => { + initializeCharCardViewer(); + }, 500); + } + + logDebug('悬浮窗按钮显示状态更新:', { + masterEnabled: isCwbEnabled(), + viewerEnabled: state.viewerEnabled, + shouldShow: shouldShow + }); } - - logDebug('悬浮窗按钮显示状态更新:', { - masterEnabled: isCwbEnabled(), - viewerEnabled: state.viewerEnabled, - shouldShow: shouldShow - }); -} -export function bindCwbApiEvents() { - console.log('[CWB] Binding API events'); - - $('#cwb-api-url').off('input').on('input', function() { - const value = $(this).val(); - extension_settings[extensionName].cwb_api_url = value; - saveSettingsDebounced(); - }); - - $('#cwb-api-key').off('input').on('input', function() { - const value = $(this).val(); - extension_settings[extensionName].cwb_api_key = value; - saveSettingsDebounced(); - }); - - $('#cwb-model').off('input').on('input', function() { - const value = $(this).val(); - extension_settings[extensionName].cwb_model = value; - saveSettingsDebounced(); - }); - - $('#cwb-temperature').off('input').on('input', function() { - const value = parseFloat($(this).val()); - $('#cwb-temperature-value').text(value); - extension_settings[extensionName].cwb_temperature = value; - saveSettingsDebounced(); - }); - - $('#cwb-max-tokens').off('input').on('input', function() { - const value = parseInt($(this).val()); - $('#cwb-max-tokens-value').text(value); - extension_settings[extensionName].cwb_max_tokens = value; - saveSettingsDebounced(); - }); - - $('#cwb-test-connection').off('click').on('click', async function() { - const $button = $(this); - $button.prop('disabled', true).html(' 测试中...'); + export function bindCwbApiEvents() { + console.log('[CWB] Binding API events'); - try { - await testCwbConnection(); - } catch (error) { - console.error('[CWB] 测试连接失败:', error); - } finally { - $button.prop('disabled', false).html(' 测试连接'); - } - }); + $('#cwb-api-url').off('input').on('input', function() { + const value = $(this).val(); + extension_settings[extensionName].cwb_api_url = value; + saveSettingsDebounced(); + }); - $('#cwb-fetch-models').off('click').on('click', async function() { - const $button = $(this); - $button.prop('disabled', true).html(' 获取中...'); - - try { - const models = await fetchCwbModels(); - const $modelSelect = $('#cwb-model'); - $modelSelect.empty(); + $('#cwb-api-key').off('input').on('input', function() { + const value = $(this).val(); + extension_settings[extensionName].cwb_api_key = value; + saveSettingsDebounced(); + }); + + $('#cwb-model').off('input').on('input', function() { + const value = $(this).val(); + extension_settings[extensionName].cwb_model = value; + saveSettingsDebounced(); + }); + + $('#cwb-temperature').off('input').on('input', function() { + const value = parseFloat($(this).val()); + $('#cwb-temperature-value').text(value); + extension_settings[extensionName].cwb_temperature = value; + saveSettingsDebounced(); + }); + + $('#cwb-max-tokens').off('input').on('input', function() { + const value = parseInt($(this).val()); + $('#cwb-max-tokens-value').text(value); + extension_settings[extensionName].cwb_max_tokens = value; + saveSettingsDebounced(); + }); + + $('#cwb-test-connection').off('click').on('click', async function() { + const $button = $(this); + $button.prop('disabled', true).html(' 测试中...'); - if (models && models.length > 0) { - models.forEach(model => { - $modelSelect.append(new Option(model.name, model.id)); - }); - showToastr('success', `已获取到 ${models.length} 个模型`); - } else { - $modelSelect.append(new Option('无可用模型', '')); - showToastr('warning', '未获取到可用模型'); + try { + await testCwbConnection(); + } catch (error) { + console.error('[CWB] 测试连接失败:', error); + } finally { + $button.prop('disabled', false).html(' 测试连接'); } - } catch (error) { - console.error('[CWB] 获取模型失败:', error); - $('#cwb-model').empty().append(new Option('获取失败', '')); - } finally { - $button.prop('disabled', false).html(' 获取模型'); - } - }); -} + }); + + $('#cwb-fetch-models').off('click').on('click', async function() { + const $button = $(this); + $button.prop('disabled', true).html(' 获取中...'); + + try { + const models = await fetchCwbModels(); + const $modelSelect = $('#cwb-model'); + $modelSelect.empty(); + + if (models && models.length > 0) { + models.forEach(model => { + $modelSelect.append(new Option(model.name, model.id)); + }); + showToastr('success', `已获取到 ${models.length} 个模型`); + } else { + $modelSelect.append(new Option('无可用模型', '')); + showToastr('warning', '未获取到可用模型'); + } + } catch (error) { + console.error('[CWB] 获取模型失败:', error); + $('#cwb-model').empty().append(new Option('获取失败', '')); + } finally { + $button.prop('disabled', false).html(' 获取模型'); + } + }); + }