Files
yawiii-ComfyUI-Prompt-Assis…/js/modules/rulesConfigManager.js
T

1402 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 规则配置管理器
* 负责管理提示词规则配置弹窗和相关功能
*/
import { app } from "../../../../scripts/app.js";
import { logger } from '../utils/logger.js';
import { ResourceManager } from "../utils/resourceManager.js";
import {
createSettingsDialog,
createFormGroup,
createInputGroup,
createSelectGroup
} from "./settings.js";
class RulesConfigManager {
constructor() {
this.systemPrompts = null;
// ---存储提示词列表数据---
this.expandPrompts = [];
this.zhVisionPrompts = [];
this.enVisionPrompts = [];
// ---当前激活的提示词ID---
this.activeExpandPromptId = null;
this.activeZhVisionPromptId = null;
this.activeEnVisionPromptId = null;
this.isDirty = false;
this.initialState = null; // 用于存储状态备份
}
/**
* 备份当前状态
*/
_backupState() {
this.initialState = {
expandPrompts: JSON.parse(JSON.stringify(this.expandPrompts)),
zhVisionPrompts: JSON.parse(JSON.stringify(this.zhVisionPrompts)),
enVisionPrompts: JSON.parse(JSON.stringify(this.enVisionPrompts)),
activeExpandPromptId: this.activeExpandPromptId,
activeZhVisionPromptId: this.activeZhVisionPromptId,
activeEnVisionPromptId: this.activeEnVisionPromptId,
};
this.isDirty = false;
// 标记表单未修改
const form = document.querySelector('.rules-config-dialog .rules-config-form');
if (form && form.elements.modified_marker) {
form.elements.modified_marker.value = 'initial';
}
}
/**
* 从备份恢复状态
*/
_restoreState() {
if (this.initialState) {
this.expandPrompts = JSON.parse(JSON.stringify(this.initialState.expandPrompts));
this.zhVisionPrompts = JSON.parse(JSON.stringify(this.initialState.zhVisionPrompts));
this.enVisionPrompts = JSON.parse(JSON.stringify(this.initialState.enVisionPrompts));
this.activeExpandPromptId = this.initialState.activeExpandPromptId;
this.activeZhVisionPromptId = this.initialState.activeZhVisionPromptId;
this.activeEnVisionPromptId = this.initialState.activeEnVisionPromptId;
}
this.isDirty = false;
}
/**
* 标记配置已修改
*/
_setDirty() {
this.isDirty = true;
// 触发表单修改,以便通用对话框检测到未保存的更改
const form = document.querySelector('.rules-config-dialog .rules-config-form');
if (form && form.elements.modified_marker) {
form.elements.modified_marker.value = 'modified_' + Date.now();
form.elements.modified_marker.dispatchEvent(new Event('change', { bubbles: true }));
}
}
/**
* 显示规则配置弹窗
*/
showRulesConfigModal() {
try {
logger.debug('打开规则配置弹窗');
createSettingsDialog({
title: '规则管理器',
dialogClassName: 'rules-config-dialog',
disableBackdropAndCloseOnClickOutside: true,
renderContent: (container) => {
this._createRulesConfigUI(container);
},
onSave: async () => {
if (this.isDirty) {
try {
await this._saveConfigToServer();
this._backupState(); // 保存成功后更新备份
app.extensionManager.toast.add({
severity: "success",
summary: "配置已保存",
detail: "所有更改已成功保存到服务器",
life: 3000
});
} catch (error) {
// 错误提示已在 _saveConfigToServer 中处理
return false; // 保存失败时阻止关闭对话框
}
} else {
app.extensionManager.toast.add({
severity: "info",
summary: "无更改",
detail: "配置没有未保存的更改",
life: 3000
});
}
return true; // 关闭对话框
},
onCancel: () => {
// createSettingsDialog 的通用逻辑会处理确认
// 如果用户确认取消,我们在这里恢复状态
if (this.isDirty) {
this._restoreState();
this._renderPromptLists(); // 重新渲染以显示恢复后的状态
}
return true; // 允许关闭
}
});
} catch (error) {
logger.error(`打开规则配置弹窗失败: ${error.message}`);
app.extensionManager.toast.add({
severity: "error",
summary: "打开配置失败",
detail: error.message || "打开配置弹窗过程中发生错误",
life: 3000
});
}
}
/**
* 创建规则配置UI
* @param {HTMLElement} container 容器元素
*/
_createRulesConfigUI(container) {
const form = document.createElement('form');
form.onsubmit = (e) => e.preventDefault();
form.className = 'rules-config-form';
// 添加一个隐藏输入,用于脏检查
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'modified_marker';
hiddenInput.value = 'initial';
form.appendChild(hiddenInput);
// 1. 扩写规则配置
const expandSection = this._createExpandSection();
// 2. 反推规则配置
const visionSection = this._createVisionSection();
form.appendChild(expandSection);
form.appendChild(visionSection);
container.appendChild(form);
// 加载系统提示词配置
this._loadSystemPrompts();
}
/**
* 创建扩写规则配置部分
* @returns {HTMLElement} 扩写规则配置部分的DOM元素
*/
_createExpandSection() {
// 扩写规则配置
const expandSection = createFormGroup('扩写规则配置');
const expandHeader = expandSection.querySelector('.settings-form-section-header');
// 创建添加按钮
const addButton = document.createElement('div');
addButton.className = 'settings-refresh-button';
addButton.title = '添加新提示词';
const addIcon = document.createElement('span');
addIcon.className = 'pi pi-plus refresh-icon';
addButton.appendChild(addIcon);
const addText = document.createElement('span');
addText.textContent = '添加提示词';
addButton.appendChild(addText);
expandHeader.appendChild(addButton);
// 创建提示词列表容器
const listContainer = document.createElement('div');
listContainer.className = 'prompt-list-container';
// 创建列表头部
const listHeader = document.createElement('div');
listHeader.className = 'prompt-list-header';
listHeader.innerHTML = `
<div class="prompt-list-cell status-cell">状态</div>
<div class="prompt-list-cell name-cell">提示词名称</div>
<div class="prompt-list-cell content-cell">提示词内容</div>
<div class="prompt-list-cell action-cell">操作</div>
`;
// 为头部单元格添加列宽调整手柄
this._addHeaderResizers(listHeader);
// 创建滚动列表
const scrollList = document.createElement('div');
scrollList.className = 'prompt-scroll-list';
listContainer.appendChild(listHeader);
listContainer.appendChild(scrollList);
expandSection.appendChild(listContainer);
// 存储列表引用
this.expandScrollList = scrollList;
// 添加调试信息
logger.debug('扩写提示词列表容器已创建');
// 处理添加按钮点击
addButton.onclick = () => {
this._showPromptEditDialog('expand', null);
};
return expandSection;
}
/**
* 创建反推规则配置部分
* @returns {HTMLElement} 反推规则配置部分的DOM元素
*/
_createVisionSection() {
// 反推规则配置
const visionSection = createFormGroup('反推规则配置');
const visionHeader = visionSection.querySelector('.settings-form-section-header');
// 创建添加按钮并添加到标题栏
const addButton = document.createElement('div');
addButton.className = 'settings-refresh-button';
addButton.title = '添加提示词';
const addIcon = document.createElement('span');
addIcon.className = 'pi pi-plus refresh-icon';
addButton.appendChild(addIcon);
const addText = document.createElement('span');
addText.textContent = '添加提示词';
addButton.appendChild(addText);
visionHeader.appendChild(addButton);
// Tab容器
const tabContainer = document.createElement('div');
tabContainer.className = 'popup_tabs_container';
const tabsScroll = document.createElement('div');
tabsScroll.className = 'popup_tabs_scroll';
const tabs = document.createElement('div');
tabs.className = 'popup_tabs';
const tabZH = document.createElement('div');
tabZH.className = 'popup_tab active';
tabZH.textContent = '中文反推规则';
const tabEN = document.createElement('div');
tabEN.className = 'popup_tab';
tabEN.textContent = '英文反推规则';
tabs.appendChild(tabZH);
tabs.appendChild(tabEN);
tabsScroll.appendChild(tabs);
tabContainer.appendChild(tabsScroll);
// 中文反推规则内容
const tabContentZH = document.createElement('div');
tabContentZH.className = 'popup_tab_content active';
// 中文列表容器
const zhListContainer = document.createElement('div');
zhListContainer.className = 'prompt-list-container vision-prompt-list-container';
const zhListHeader = document.createElement('div');
zhListHeader.className = 'prompt-list-header';
zhListHeader.innerHTML = `
<div class="prompt-list-cell status-cell">状态</div>
<div class="prompt-list-cell name-cell">提示词名称</div>
<div class="prompt-list-cell content-cell">提示词内容</div>
<div class="prompt-list-cell action-cell">操作</div>
`;
// 为头部单元格添加列宽调整手柄
this._addHeaderResizers(zhListHeader);
const zhScrollList = document.createElement('div');
zhScrollList.className = 'prompt-scroll-list';
zhListContainer.appendChild(zhListHeader);
zhListContainer.appendChild(zhScrollList);
tabContentZH.appendChild(zhListContainer);
// 存储中文列表引用
this.zhVisionScrollList = zhScrollList;
// 添加调试信息
logger.debug('中文反推提示词列表容器已创建');
// 英文反推规则内容
const tabContentEN = document.createElement('div');
tabContentEN.className = 'popup_tab_content';
// 英文列表容器
const enListContainer = document.createElement('div');
enListContainer.className = 'prompt-list-container vision-prompt-list-container';
const enListHeader = document.createElement('div');
enListHeader.className = 'prompt-list-header';
enListHeader.innerHTML = `
<div class="prompt-list-cell status-cell">状态</div>
<div class="prompt-list-cell name-cell">提示词名称</div>
<div class="prompt-list-cell content-cell">提示词内容</div>
<div class="prompt-list-cell action-cell">操作</div>
`;
// 为头部单元格添加列宽调整手柄
this._addHeaderResizers(enListHeader);
const enScrollList = document.createElement('div');
enScrollList.className = 'prompt-scroll-list';
enListContainer.appendChild(enListHeader);
enListContainer.appendChild(enScrollList);
tabContentEN.appendChild(enListContainer);
// 存储英文列表引用
this.enVisionScrollList = enScrollList;
// 添加调试信息
logger.debug('英文反推提示词列表容器已创建');
// 设置Tab切换逻辑
tabZH.onclick = () => {
tabZH.classList.add('active');
tabEN.classList.remove('active');
tabContentZH.classList.add('active');
tabContentEN.classList.remove('active');
};
tabEN.onclick = () => {
tabEN.classList.add('active');
tabZH.classList.remove('active');
tabContentEN.classList.add('active');
tabContentZH.classList.remove('active');
};
visionSection.appendChild(tabContainer);
visionSection.appendChild(tabContentZH);
visionSection.appendChild(tabContentEN);
// 处理添加按钮点击 - 根据当前激活的tab决定类型
addButton.onclick = () => {
const activeTab = tabs.querySelector('.popup_tab.active');
const isZhActive = activeTab && activeTab.textContent === '中文反推规则';
this._showPromptEditDialog(isZhActive ? 'zhVision' : 'enVision', null);
};
return visionSection;
}
/**
* 加载系统提示词配置
*/
async _loadSystemPrompts() {
try {
const response = await fetch('/prompt_assistant/api/config/system_prompts');
if (!response.ok) {
throw new Error(`加载系统提示词配置失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
this.systemPrompts = data;
// 获取激活的提示词ID
const activePrompts = data.active_prompts || {};
const activeExpandId = activePrompts.expand;
const activeZhVisionId = activePrompts.vision_zh;
const activeEnVisionId = activePrompts.vision_en;
// 输出加载的激活ID
logger.debug(`加载的激活提示词ID | expand:${activeExpandId || '无'} | vision_zh:${activeZhVisionId || '无'} | vision_en:${activeEnVisionId || '无'}`);
// 转换扩写规则数据
this.expandPrompts = [];
if (data.expand_prompts) {
// 保存原始顺序,以便在保存时恢复
this.originalExpandOrder = Object.keys(data.expand_prompts);
// 使用原始顺序遍历键
this.originalExpandOrder.forEach(key => {
const prompt = data.expand_prompts[key];
// 使用配置中的键作为ID,而不是生成新的ID
this.expandPrompts.push({
id: key,
name: prompt.name || key, // 如果没有名称,使用键作为名称
content: prompt.content,
isActive: key === activeExpandId, // 根据active_prompts判断是否激活
order: this.originalExpandOrder.indexOf(key) // 保存原始顺序
});
});
// 如果没有找到激活的提示词,则激活第一个
if (!activeExpandId && this.expandPrompts.length > 0) {
this.expandPrompts[0].isActive = true;
this.activeExpandPromptId = this.expandPrompts[0].id;
} else {
this.activeExpandPromptId = activeExpandId;
}
// 按原始顺序排序
this.expandPrompts.sort((a, b) => a.order - b.order);
}
// 转换反推规则数据
this.zhVisionPrompts = [];
this.enVisionPrompts = [];
if (data.vision_prompts) {
// 保存原始顺序
this.originalVisionOrder = Object.keys(data.vision_prompts);
// 分别存储中文和英文提示词的键
const zhKeys = [];
const enKeys = [];
// 先分类所有键
this.originalVisionOrder.forEach(key => {
const prompt = data.vision_prompts[key];
// 根据ID或内容判断是中文还是英文
const isChinese = key.includes('zh') || /[\u4e00-\u9fa5]/.test(prompt.content);
if (isChinese) {
zhKeys.push(key);
} else {
enKeys.push(key);
}
});
// 处理中文提示词
zhKeys.forEach((key, index) => {
const prompt = data.vision_prompts[key];
this.zhVisionPrompts.push({
id: key,
name: prompt.name || key,
content: prompt.content,
isActive: key === activeZhVisionId,
order: this.originalVisionOrder.indexOf(key) // 保存原始顺序
});
});
// 处理英文提示词
enKeys.forEach((key, index) => {
const prompt = data.vision_prompts[key];
this.enVisionPrompts.push({
id: key,
name: prompt.name || key,
content: prompt.content,
isActive: key === activeEnVisionId,
order: this.originalVisionOrder.indexOf(key) // 保存原始顺序
});
});
// 按原始顺序排序
this.zhVisionPrompts.sort((a, b) => a.order - b.order);
this.enVisionPrompts.sort((a, b) => a.order - b.order);
// 如果没有找到激活的中文提示词,则激活第一个
if (!activeZhVisionId && this.zhVisionPrompts.length > 0) {
this.zhVisionPrompts[0].isActive = true;
this.activeZhVisionPromptId = this.zhVisionPrompts[0].id;
} else {
this.activeZhVisionPromptId = activeZhVisionId;
}
// 如果没有找到激活的英文提示词,则激活第一个
if (!activeEnVisionId && this.enVisionPrompts.length > 0) {
this.enVisionPrompts[0].isActive = true;
this.activeEnVisionPromptId = this.enVisionPrompts[0].id;
} else {
this.activeEnVisionPromptId = activeEnVisionId;
}
}
// 备份初始状态
this._backupState();
// 渲染列表
this._renderPromptLists();
} catch (error) {
logger.error("加载系统提示词配置失败:", error);
app.extensionManager.toast.add({
severity: "error",
summary: "加载失败",
detail: error.message || "加载系统提示词配置过程中发生错误",
life: 3000
});
}
}
/**
* 渲染提示词列表
*/
_renderPromptLists() {
logger.debug(`开始渲染提示词列表 | 扩写提示词:${this.expandPrompts.length}个 | 中文反推:${this.zhVisionPrompts.length}个 | 英文反推:${this.enVisionPrompts.length}个`);
// 渲染扩写规则列表
if (this.expandScrollList) {
this._renderPromptList(this.expandScrollList, this.expandPrompts, 'expand');
logger.debug(`渲染扩写提示词列表完成`);
} else {
logger.error(`扩写提示词列表容器不存在`);
}
// 渲染中文反推规则列表
if (this.zhVisionScrollList) {
this._renderPromptList(this.zhVisionScrollList, this.zhVisionPrompts, 'zhVision');
logger.debug(`渲染中文反推提示词列表完成`);
} else {
logger.error(`中文反推提示词列表容器不存在`);
}
// 渲染英文反推规则列表
if (this.enVisionScrollList) {
this._renderPromptList(this.enVisionScrollList, this.enVisionPrompts, 'enVision');
logger.debug(`渲染英文反推提示词列表完成`);
} else {
logger.error(`英文反推提示词列表容器不存在`);
}
}
/**
* 渲染单个提示词列表
* @param {HTMLElement} container 列表容器
* @param {Array} prompts 提示词数组
* @param {string} type 类型
*/
_renderPromptList(container, prompts, type) {
container.innerHTML = '';
prompts.forEach(prompt => {
const row = document.createElement('div');
row.className = 'prompt-list-row';
row.dataset.promptId = prompt.id;
row.dataset.type = type;
row.dataset.order = prompt.order; // 添加顺序属性到DOM元素,方便调试
// 状态列 - 可点击切换状态
const statusCell = document.createElement('div');
statusCell.className = 'prompt-list-cell status-cell';
const statusIcon = document.createElement('span');
statusIcon.className = prompt.isActive ? 'pi pi-check-circle active-status' : 'pi pi-circle-off inactive-status';
statusIcon.title = prompt.isActive ? '当前使用中,点击取消激活' : '未激活,点击激活';
statusCell.appendChild(statusIcon);
statusCell.onclick = (e) => {
e.stopPropagation();
this._togglePromptActive(type, prompt.id);
};
// 名称列
const nameCell = document.createElement('div');
nameCell.className = 'prompt-list-cell name-cell';
// 使用name属性显示名称,而不是id
nameCell.textContent = prompt.name;
nameCell.title = prompt.name;
// 添加列宽调整手柄
const nameResizer = document.createElement('div');
nameResizer.className = 'column-resizer';
nameCell.appendChild(nameResizer);
this._addColumnResizer(nameResizer, nameCell);
// 内容列
const contentCell = document.createElement('div');
contentCell.className = 'prompt-list-cell content-cell';
const contentPreview = prompt.content.length > 50 ?
prompt.content.substring(0, 50) + '...' : prompt.content;
contentCell.textContent = contentPreview;
contentCell.title = prompt.content;
// 添加列宽调整手柄
const contentResizer = document.createElement('div');
contentResizer.className = 'column-resizer';
contentCell.appendChild(contentResizer);
this._addColumnResizer(contentResizer, contentCell);
// 操作列
const actionCell = document.createElement('div');
actionCell.className = 'prompt-list-cell action-cell';
// 编辑按钮
const editButton = document.createElement('button');
editButton.className = 'prompt-action-btn edit-btn';
editButton.innerHTML = '<span class="pi pi-pencil"></span>';
editButton.title = '编辑';
editButton.onclick = (e) => {
e.stopPropagation();
this._showPromptEditDialog(type, prompt.id);
};
// 删除按钮
const deleteButton = document.createElement('button');
deleteButton.className = 'prompt-action-btn delete-btn';
deleteButton.innerHTML = '<span class="pi pi-trash"></span>';
deleteButton.title = '删除';
deleteButton.onclick = (e) => {
e.stopPropagation();
this._deletePrompt(type, prompt.id);
};
actionCell.appendChild(editButton);
actionCell.appendChild(deleteButton);
row.appendChild(statusCell);
row.appendChild(nameCell);
row.appendChild(contentCell);
row.appendChild(actionCell);
container.appendChild(row);
});
// 初始化拖拽排序
this._initSortable(container, type);
}
/**
* 显示提示词编辑对话框
* @param {string} type 类型
* @param {string|null} promptId 提示词ID,null表示新建
*/
_showPromptEditDialog(type, promptId) {
this._showPromptConfigDialog(type, promptId);
}
/**
* 创建多行文本输入框组
* @param {string} label 标签文本
* @param {string} placeholder 占位符文本
* @returns {Object} 包含 group 和 textarea 的对象
*/
_createTextareaGroup(label, placeholder) {
const group = document.createElement('div');
group.className = 'settings-form-group';
const labelElem = document.createElement('label');
labelElem.className = 'settings-form-label';
labelElem.textContent = label;
const textarea = document.createElement('textarea');
textarea.className = 'p-inputtext p-component settings-form-textarea';
textarea.placeholder = placeholder;
textarea.rows = 8;
textarea.style.resize = 'vertical';
textarea.style.minHeight = '150px';
textarea.style.height = 'calc(100% - 30px)';
group.appendChild(labelElem);
group.appendChild(textarea);
return { group, textarea };
}
/**
* 显示通用的提示词配置弹窗
* @param {string} defaultType 默认类型
* @param {string|null} promptId 提示词ID,null表示新建
*/
_showPromptConfigDialog(defaultType = 'expand', promptId = null) {
const isEdit = !!promptId;
let editData = null;
// 如果是编辑模式,查找对应的数据
if (isEdit) {
if (defaultType === 'expand') {
editData = this.expandPrompts.find(p => p.id === promptId);
} else if (defaultType === 'zhVision') {
editData = this.zhVisionPrompts.find(p => p.id === promptId);
} else if (defaultType === 'enVision') {
editData = this.enVisionPrompts.find(p => p.id === promptId);
}
}
createSettingsDialog({
title: isEdit ? '编辑提示词' : '添加提示词',
isConfirmDialog: true,
dialogClassName: 'prompt-edit-dialog',
disableBackdropAndCloseOnClickOutside: true,
saveButtonText: isEdit ? '保存' : '添加',
cancelButtonText: '取消',
renderContent: (content) => {
content.className += ' dialog-form-content';
content.style.padding = '1rem';
content.style.display = 'flex';
content.style.flexDirection = 'column';
content.style.flex = '1';
// 创建提示词名称输入框
const nameInput = createInputGroup('提示词名称', '请输入提示词名称');
nameInput.group.style.marginBottom = '10px';
if (isEdit && editData) {
nameInput.input.value = editData.name || '';
}
// 创建提示词类型下拉框
const typeOptions = [
{ value: 'expand', text: '扩写' },
{ value: 'zhVision', text: '反推(中文)' },
{ value: 'enVision', text: '反推(英文)' }
];
const typeSelect = createSelectGroup('提示词类型', typeOptions, defaultType);
typeSelect.group.style.marginBottom = '10px';
// 创建提示词内容多行输入框
const contentTextarea = this._createTextareaGroup('提示词内容', '请输入提示词内容');
contentTextarea.group.style.marginBottom = '0';
contentTextarea.group.style.flex = '1';
contentTextarea.group.style.display = 'flex';
contentTextarea.group.style.flexDirection = 'column';
if (isEdit && editData) {
contentTextarea.textarea.value = editData.content || '';
}
content.appendChild(nameInput.group);
content.appendChild(typeSelect.group);
content.appendChild(contentTextarea.group);
content.nameInput = nameInput.input;
content.typeSelect = typeSelect.select;
content.contentTextarea = contentTextarea.textarea;
// 聚焦名称输入框
setTimeout(() => {
nameInput.input.focus();
}, 100);
},
onSave: (content) => {
const name = content.nameInput.value.trim();
const type = content.typeSelect.value;
const promptContent = content.contentTextarea.value.trim();
if (!name || !promptContent) {
app.extensionManager.toast.add({
severity: "error",
summary: isEdit ? "编辑失败" : "添加失败",
detail: "提示词名称和内容不能为空",
life: 3000
});
return;
}
// 根据类型获取对应的数组
let targetArray;
if (type === 'expand') {
targetArray = this.expandPrompts;
} else if (type === 'zhVision') {
targetArray = this.zhVisionPrompts;
} else if (type === 'enVision') {
targetArray = this.enVisionPrompts;
} else {
logger.error(`未知的提示词类型: ${type}`);
return;
}
// 检查名称是否重复(排除编辑时的自身)
const isDuplicate = targetArray.some(p =>
(p.name === name || p.id === name) && (!isEdit || p.id !== promptId)
);
if (isDuplicate) {
app.extensionManager.toast.add({
severity: "error",
summary: isEdit ? "编辑失败" : "添加失败",
detail: `提示词名称"${name}"已存在`,
life: 3000
});
return;
}
if (isEdit) {
// 编辑模式:更新现有数据
if (editData) {
// 如果类型发生变化,需要从原数组中移除,添加到新数组
if (type !== defaultType) {
// 从原数组中移除
let originalArray;
if (defaultType === 'expand') {
originalArray = this.expandPrompts;
} else if (defaultType === 'zhVision') {
originalArray = this.zhVisionPrompts;
} else if (defaultType === 'enVision') {
originalArray = this.enVisionPrompts;
}
const index = originalArray.findIndex(p => p.id === promptId);
if (index !== -1) {
originalArray.splice(index, 1);
}
// 添加到新数组
// 为不同类型的提示词设置不同的ID前缀
let newId;
if (type === 'expand') {
newId = 'expand_' + name.replace(/\s+/g, '_');
} else if (type === 'zhVision') {
newId = 'vision_zh_' + name.replace(/\s+/g, '_');
} else if (type === 'enVision') {
newId = 'vision_en_' + name.replace(/\s+/g, '_');
} else {
newId = this._generateId();
}
const newPrompt = {
id: newId,
name: name,
content: promptContent,
isActive: false,
order: targetArray.length // 设置顺序为当前数组长度
};
targetArray.push(newPrompt);
logger.debug(`跨类型编辑提示词 | 原类型:${defaultType} | 新类型:${type} | 新ID:${newId}`);
} else {
// 同类型编辑,直接更新
const oldName = editData.name;
editData.name = name;
editData.content = promptContent;
// 如果名称发生了变化,则更新ID
if (oldName !== name) {
const oldId = editData.id;
let newId;
// 根据类型设置不同的ID前缀
if (type === 'expand') {
newId = 'expand_' + name.replace(/\s+/g, '_');
} else if (type === 'zhVision') {
newId = 'vision_zh_' + name.replace(/\s+/g, '_');
} else if (type === 'enVision') {
newId = 'vision_en_' + name.replace(/\s+/g, '_');
} else {
newId = name;
}
editData.id = newId;
logger.debug(`更新提示词ID | 类型:${type} | 旧ID:${oldId} | 新ID:${newId}`);
}
}
}
} else {
// 添加模式:创建新数据
// 根据类型设置不同的ID前缀
let newId;
if (type === 'expand') {
newId = 'expand_' + name.replace(/\s+/g, '_');
} else if (type === 'zhVision') {
newId = 'vision_zh_' + name.replace(/\s+/g, '_');
} else if (type === 'enVision') {
newId = 'vision_en_' + name.replace(/\s+/g, '_');
} else {
newId = name;
}
const newPrompt = {
id: newId,
name: name,
content: promptContent,
isActive: false, // 默认不激活
order: targetArray.length // 设置顺序为当前数组长度,确保添加到末尾
};
targetArray.push(newPrompt);
// 输出调试信息
logger.debug(`添加新提示词 | 类型:${type} | ID:${newId} | 顺序:${newPrompt.order} | 数组长度:${targetArray.length}`);
}
// 标记为已修改
this._setDirty();
// 重新渲染对应的列表
this._renderPromptLists();
return true;
}
});
}
/**
* 生成唯一ID
* @returns {string} 唯一ID
*/
_generateId() {
return 'prompt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
/**
* 刷新所有提示词列表
* 注意:为了保持一致性,建议直接使用_renderPromptLists方法
*/
_refreshPromptLists() {
// 直接调用_renderPromptLists方法,确保一致性
this._renderPromptLists();
}
/**
* 切换提示词激活状态
* @param {string} type 类型
* @param {string} promptId 提示词ID
*/
_togglePromptActive(type, promptId) {
// 获取对应的数据数组
let dataArray;
let activeIdProperty;
let configKey; // 用于保存到配置中的键名
switch (type) {
case 'expand':
dataArray = this.expandPrompts;
activeIdProperty = 'activeExpandPromptId';
configKey = 'expand';
break;
case 'zhVision':
dataArray = this.zhVisionPrompts;
activeIdProperty = 'activeZhVisionPromptId';
configKey = 'vision_zh';
break;
case 'enVision':
dataArray = this.enVisionPrompts;
activeIdProperty = 'activeEnVisionPromptId';
configKey = 'vision_en';
break;
default:
return;
}
// 找到目标提示词
const targetPrompt = dataArray.find(p => p.id === promptId);
if (!targetPrompt) return;
// 记录之前的激活状态
const oldActiveId = this[activeIdProperty];
const wasActive = targetPrompt.isActive;
// 如果当前提示词已激活,则取消激活
if (targetPrompt.isActive) {
targetPrompt.isActive = false;
this[activeIdProperty] = null;
logger.debug(`取消激活提示词 | 类型:${type} | ID:${promptId} | 配置键:${configKey}`);
} else {
// 取消其他提示词的激活状态
dataArray.forEach(p => p.isActive = false);
// 激活当前提示词
targetPrompt.isActive = true;
this[activeIdProperty] = promptId;
logger.debug(`激活提示词 | 类型:${type} | ID:${promptId} | 配置键:${configKey} | 旧ID:${oldActiveId}`);
}
// 标记为已修改
this._setDirty();
// 重新渲染列表
this._renderPromptLists();
// 输出激活状态变化
logger.debug(`激活状态变化 | 类型:${type} | 提示词:${targetPrompt.name} | ID:${promptId} | 旧状态:${wasActive} | 新状态:${targetPrompt.isActive} | 实例属性:${this[activeIdProperty]}`);
// 如果系统提示词对象存在,直接更新其中的active_prompts
if (this.systemPrompts && this.systemPrompts.active_prompts) {
const oldValue = this.systemPrompts.active_prompts[configKey];
this.systemPrompts.active_prompts[configKey] = targetPrompt.isActive ? promptId : null;
logger.debug(`更新系统提示词对象 | 配置键:${configKey} | 旧值:${oldValue} | 新值:${this.systemPrompts.active_prompts[configKey]}`);
}
logger.debug(`${type} 提示词状态切换: ${promptId} -> ${targetPrompt.isActive ? '激活' : '取消激活'}`);
}
/**
* 删除提示词
* @param {string} type 类型
* @param {string} promptId 提示词ID
*/
_deletePrompt(type, promptId) {
// 获取对应的数据数组
let dataArray;
switch (type) {
case 'expand':
dataArray = this.expandPrompts;
break;
case 'zhVision':
dataArray = this.zhVisionPrompts;
break;
case 'enVision':
dataArray = this.enVisionPrompts;
break;
default:
return;
}
// 找到要删除的提示词索引
const index = dataArray.findIndex(p => p.id === promptId);
if (index === -1) return;
// 获取提示词名称用于提示
const promptName = dataArray[index].name;
// 创建确认对话框
createSettingsDialog({
title: '确认删除',
isConfirmDialog: true,
disableBackdropAndCloseOnClickOutside: true,
saveButtonText: '确认',
cancelButtonText: '取消',
renderContent: (content) => {
content.style.textAlign = 'center';
content.style.padding = '1rem';
const confirmMessage = document.createElement('p');
confirmMessage.textContent = `确定要删除提示词"${promptName}"吗?`;
confirmMessage.style.margin = '0';
confirmMessage.style.fontSize = '1rem';
content.appendChild(confirmMessage);
},
onSave: () => {
// 从数组中删除提示词
dataArray.splice(index, 1);
// 标记为已修改
this._setDirty();
// 重新渲染列表
this._renderPromptLists();
return true;
}
});
}
/**
* 将当前配置保存到服务器
* @returns {Promise} 保存操作的Promise
*/
async _saveConfigToServer() {
try {
// 构建系统提示词配置
const systemPrompts = {
expand_prompts: {},
translate_prompts: {
ZH: {
role: "system",
content: "你是一名AI绘画领域的提示词翻译专家,负责将用户提供的文本内容由{src_lang}准确地翻译成{dst_lang}。要求:1.完整翻译用户提供的所有文本,不要遗漏;2.保持格式,不要改变原文的书写结构、标点符号、权重标记格式【如(文本内容:1.2)】等;2.准确原文,使用准确、地道的{dst_lang}表达词汇和AI绘画领域的专业的术语;5.直接输出翻译结果,无需注释、说明。"
}
},
vision_prompts: {},
active_prompts: {
expand: null,
vision_zh: null,
vision_en: null
}
};
// 创建一个有序的提示词列表
const orderedExpandPrompts = [...this.expandPrompts].sort((a, b) => a.order - b.order);
// 添加扩写提示词,并记录激活的提示词ID
orderedExpandPrompts.forEach(prompt => {
// 使用提示词ID作为键,而不是名称
systemPrompts.expand_prompts[prompt.id] = {
name: prompt.name,
role: "system",
content: prompt.content
};
// 记录激活的提示词ID
if (prompt.isActive) {
systemPrompts.active_prompts.expand = prompt.id;
logger.debug(`保存激活的扩写提示词ID: ${prompt.id}`);
}
});
// 如果没有找到激活的扩写提示词,使用实例属性中保存的ID
if (!systemPrompts.active_prompts.expand && this.activeExpandPromptId) {
systemPrompts.active_prompts.expand = this.activeExpandPromptId;
logger.debug(`使用实例属性中的扩写提示词ID: ${this.activeExpandPromptId}`);
}
// 创建有序的中文和英文反推提示词列表
const orderedZhVisionPrompts = [...this.zhVisionPrompts].sort((a, b) => a.order - b.order);
const orderedEnVisionPrompts = [...this.enVisionPrompts].sort((a, b) => a.order - b.order);
// 添加中文反推提示词
orderedZhVisionPrompts.forEach(prompt => {
systemPrompts.vision_prompts[prompt.id] = {
name: prompt.name,
role: "system",
content: prompt.content
};
// 记录激活的提示词ID
if (prompt.isActive) {
systemPrompts.active_prompts.vision_zh = prompt.id;
logger.debug(`保存激活的中文反推提示词ID: ${prompt.id}`);
}
});
// 如果没有找到激活的中文反推提示词,使用实例属性中保存的ID
if (!systemPrompts.active_prompts.vision_zh && this.activeZhVisionPromptId) {
systemPrompts.active_prompts.vision_zh = this.activeZhVisionPromptId;
logger.debug(`使用实例属性中的中文反推提示词ID: ${this.activeZhVisionPromptId}`);
}
// 添加英文反推提示词
orderedEnVisionPrompts.forEach(prompt => {
systemPrompts.vision_prompts[prompt.id] = {
name: prompt.name,
role: "system",
content: prompt.content
};
// 记录激活的提示词ID
if (prompt.isActive) {
systemPrompts.active_prompts.vision_en = prompt.id;
logger.debug(`保存激活的英文反推提示词ID: ${prompt.id}`);
}
});
// 如果没有找到激活的英文反推提示词,使用实例属性中保存的ID
if (!systemPrompts.active_prompts.vision_en && this.activeEnVisionPromptId) {
systemPrompts.active_prompts.vision_en = this.activeEnVisionPromptId;
logger.debug(`使用实例属性中的英文反推提示词ID: ${this.activeEnVisionPromptId}`);
}
// 输出最终的激活状态
logger.debug(`最终激活的提示词ID: expand=${systemPrompts.active_prompts.expand}, vision_zh=${systemPrompts.active_prompts.vision_zh}, vision_en=${systemPrompts.active_prompts.vision_en}`);
const response = await fetch('/prompt_assistant/api/config/system_prompts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(systemPrompts)
});
if (!response.ok) {
throw new Error(`保存失败: ${response.status} ${response.statusText}`);
}
logger.debug('配置已成功保存到服务器');
return true;
} catch (error) {
logger.error(`保存系统提示词配置失败: ${error.message}`);
throw error;
}
}
/**
* 为头部单元格添加列宽调整手柄
* @param {HTMLElement} headerRow 头部行元素
*/
_addHeaderResizers(headerRow) {
const cells = headerRow.querySelectorAll('.prompt-list-cell');
// 为前三列添加调整手柄(最后一列操作列不需要)
for (let i = 0; i < cells.length - 1; i++) {
const cell = cells[i];
const resizer = document.createElement('div');
resizer.className = 'column-resizer';
cell.appendChild(resizer);
this._addColumnResizer(resizer, cell);
}
}
/**
* 添加列宽调整功能
* @param {HTMLElement} resizer 调整手柄元素
* @param {HTMLElement} column 列元素
*/
_addColumnResizer(resizer, column) {
let isResizing = false;
let startX = 0;
let startWidth = 0;
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startWidth = parseInt(document.defaultView.getComputedStyle(column).width, 10);
// 添加拖拽状态样式
resizer.classList.add('resizing');
document.body.classList.add('column-resizing');
// 添加事件监听器
document.addEventListener('mousemove', handleMouseMove, { capture: true });
document.addEventListener('mouseup', handleMouseUp, { capture: true });
e.preventDefault();
e.stopPropagation();
});
const handleMouseMove = (e) => {
if (!isResizing) return;
e.preventDefault();
e.stopPropagation();
const deltaX = e.clientX - startX;
const newWidth = startWidth + deltaX;
// 设置最小和最大宽度限制
const minWidth = 80;
const maxWidth = 500;
if (newWidth >= minWidth && newWidth <= maxWidth) {
// 获取列的类名以确定是哪一列
const columnClass = Array.from(column.classList).find(cls =>
cls.includes('-cell') && cls !== 'prompt-list-cell'
);
if (columnClass) {
// 同时调整所有相同类型的列
const allColumns = document.querySelectorAll(`.${columnClass}`);
allColumns.forEach(col => {
col.style.width = newWidth + 'px';
col.style.flexShrink = '0';
col.style.flexGrow = '0';
col.style.flexBasis = newWidth + 'px';
});
} else {
// 如果没有找到特定类名,只调整当前列
column.style.width = newWidth + 'px';
column.style.flexShrink = '0';
column.style.flexGrow = '0';
column.style.flexBasis = newWidth + 'px';
}
}
};
const handleMouseUp = (e) => {
if (!isResizing) return;
isResizing = false;
// 获取列的类名以确定是哪一列
const columnClass = Array.from(column.classList).find(cls =>
cls.includes('-cell') && cls !== 'prompt-list-cell'
);
// 重置 flex 属性,允许下次调整
if (columnClass) {
const allColumns = document.querySelectorAll(`.${columnClass}`);
allColumns.forEach(col => {
col.style.flexShrink = '';
col.style.flexGrow = '';
col.style.flexBasis = '';
});
} else {
column.style.flexShrink = '';
column.style.flexGrow = '';
column.style.flexBasis = '';
}
// 移除拖拽状态样式
resizer.classList.remove('resizing');
document.body.classList.remove('column-resizing');
// 移除事件监听器
document.removeEventListener('mousemove', handleMouseMove, { capture: true });
document.removeEventListener('mouseup', handleMouseUp, { capture: true });
e.preventDefault();
e.stopPropagation();
};
// 添加鼠标进入和离开事件以增强视觉反馈
resizer.addEventListener('mouseenter', () => {
if (!isResizing) {
resizer.style.opacity = '0.6';
}
});
resizer.addEventListener('mouseleave', () => {
if (!isResizing) {
resizer.style.opacity = '';
}
});
}
/**
* 初始化拖拽排序功能
* @param {HTMLElement} container 列表容器
* @param {string} type 类型
*/
_initSortable(container, type) {
// 检查是否已加载Sortable库
if (typeof Sortable === 'undefined') {
this._loadSortableLibrary().then(() => {
this._createSortableInstance(container, type);
}).catch(error => {
logger.error('加载Sortable库失败:', error);
});
} else {
this._createSortableInstance(container, type);
}
}
/**
* 加载Sortable库
* @returns {Promise}
*/
_loadSortableLibrary() {
return new Promise((resolve, reject) => {
if (typeof Sortable !== 'undefined') {
resolve();
return;
}
const script = document.createElement('script');
script.src = '/custom_nodes/comfyui_prompt_assistant/js/lib/Sortable.min.js';
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load Sortable library'));
document.head.appendChild(script);
});
}
/**
* 创建Sortable实例
* @param {HTMLElement} container 列表容器
* @param {string} type 类型
*/
_createSortableInstance(container, type) {
new Sortable(container, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
handle: '.prompt-list-row',
// 移除 handle 限制,允许整行拖拽
// handle: '.prompt-list-row',
// 禁用固定项目,确保所有项目都可以拖拽
filter: '',
preventOnFilter: false,
onStart: (evt) => {
logger.debug(`开始拖拽: 类型=${type}, 索引=${evt.oldIndex}`);
},
onEnd: (evt) => {
logger.debug(`结束拖拽: 类型=${type}, 旧索引=${evt.oldIndex}, 新索引=${evt.newIndex}`);
this._handleSortEnd(evt, type);
}
});
}
/**
* 处理拖拽排序结束事件
* @param {Event} evt 排序事件
* @param {string} type 类型
*/
_handleSortEnd(evt, type) {
const { oldIndex, newIndex } = evt;
if (oldIndex === newIndex) return;
// 获取对应的数据数组
let dataArray;
switch (type) {
case 'expand':
dataArray = this.expandPrompts;
break;
case 'zhVision':
dataArray = this.zhVisionPrompts;
break;
case 'enVision':
dataArray = this.enVisionPrompts;
break;
default:
return;
}
// 输出排序前的顺序信息
logger.debug(`排序前数据: ${JSON.stringify(dataArray.map(item => ({ id: item.id, order: item.order })))}`);
// 重新排序数据
const movedItem = dataArray.splice(oldIndex, 1)[0];
dataArray.splice(newIndex, 0, movedItem);
// 更新所有项目的顺序属性
dataArray.forEach((item, index) => {
item.order = index;
});
// 输出排序后的顺序信息
logger.debug(`排序后数据: ${JSON.stringify(dataArray.map(item => ({ id: item.id, order: item.order })))}`);
logger.debug(`${type} 列表排序已更新: ${oldIndex} -> ${newIndex} | 移动项ID: ${movedItem.id}`);
// 标记为已修改
this._setDirty();
}
}
// 导出规则配置管理器实例
export const rulesConfigManager = new RulesConfigManager();