// ui/prompt-pool-ui.js // 翻译提示词池 UI 控制器 /** * 提示词池 UI 管理器 */ class PromptPoolUI { constructor() { this.promptPool = window.translationPromptPool; this.currentEditingId = null; this.settingsKey = 'promptPoolSettings'; // localStorage键名 this.sessionLockedPrompt = null; // 会话内固定提示词 this.selectedIds = new Set(); // 多选集合 // 排序/过滤默认值 this.sortKey = null; this.sortAsc = true; this.filterHealth = 'all'; this.searchKeyword = ''; // 先加载设置,再绑定事件,再根据设置渲染 this.loadSettings(); this.initializeEventListeners(); this.handlePromptModeChange(); this.updateUI(); // 后端同步事件:Prompt Pool 更新后刷新界面 try { window.addEventListener('pb:prompt-pool-updated', () => { this.updateUI(); }); } catch {} // 延迟加载模型列表,确保其他脚本已加载 setTimeout(() => { this.populateAvailableModels(); }, 1000); } /** * 加载保存的设置 */ loadSettings() { try { const savedSettings = localStorage.getItem(this.settingsKey); if (savedSettings) { const settings = JSON.parse(savedSettings); // 恢复提示词模式 if (settings.promptMode) { const modeRadio = document.getElementById(`promptMode${settings.promptMode.charAt(0).toUpperCase() + settings.promptMode.slice(1)}`); if (modeRadio) { modeRadio.checked = true; } } // 恢复参考提示词 if (settings.referenceSystemPrompt) { const systemPromptEl = document.getElementById('referenceSystemPrompt'); if (systemPromptEl) systemPromptEl.value = settings.referenceSystemPrompt; } if (settings.referenceUserPrompt) { const userPromptEl = document.getElementById('referenceUserPrompt'); if (userPromptEl) userPromptEl.value = settings.referenceUserPrompt; } // 恢复生成参数 if (settings.variationCount) { const countEl = document.getElementById('variationCount'); if (countEl) countEl.value = settings.variationCount; } if (settings.similarityControl) { const similarityEl = document.getElementById('similarityControl'); if (similarityEl) similarityEl.value = settings.similarityControl; } // 恢复生成并发 if (settings.generationConcurrency) { const ccEl = document.getElementById('generationConcurrency'); if (ccEl) ccEl.value = settings.generationConcurrency; } if (settings.generationModel) { const modelEl = document.getElementById('generationModel'); if (modelEl) { // 延迟设置,等模型列表加载完成 setTimeout(() => { modelEl.value = settings.generationModel; }, 1500); } } // 恢复提示词池模式 if (settings.promptPoolMode) { const poolModeEl = document.getElementById('promptPoolMode'); if (poolModeEl) poolModeEl.value = settings.promptPoolMode; } // 恢复生成器元提示词;若没有保存值则填入默认 const sysEl = document.getElementById('generatorSystemPrompt'); const userEl = document.getElementById('generatorUserPrompt'); if (sysEl) sysEl.value = settings.generatorSystemPrompt || this.buildDefaultGeneratorSystemPrompt(); if (userEl) userEl.value = settings.generatorUserPrompt || this.buildDefaultGeneratorUserPrompt(); // 恢复生成并发 if (settings.generationConcurrency) { const ccEl = document.getElementById('generationConcurrency'); if (ccEl) ccEl.value = settings.generationConcurrency; } // 恢复生成语言 const langEl = document.getElementById('generatorLanguage'); if (langEl) langEl.value = settings.generatorLanguage || '中文'; } } catch (error) { console.error('[PromptPoolUI] 加载设置失败:', error); } } /** * 保存当前设置 */ saveSettings() { try { const settings = { // 提示词模式 promptMode: document.querySelector('input[name="promptMode"]:checked')?.value, // 参考提示词 referenceSystemPrompt: document.getElementById('referenceSystemPrompt')?.value, referenceUserPrompt: document.getElementById('referenceUserPrompt')?.value, // 生成参数 variationCount: document.getElementById('variationCount')?.value, similarityControl: document.getElementById('similarityControl')?.value, generationModel: document.getElementById('generationModel')?.value, generationConcurrency: document.getElementById('generationConcurrency')?.value, generatorLanguage: document.getElementById('generatorLanguage')?.value || '中文', // 提示词池模式 promptPoolMode: document.getElementById('promptPoolMode')?.value, // 生成器元提示词(可选) generatorSystemPrompt: document.getElementById('generatorSystemPrompt')?.value, generatorUserPrompt: document.getElementById('generatorUserPrompt')?.value, // 保存时间戳 savedAt: Date.now() }; localStorage.setItem(this.settingsKey, JSON.stringify(settings)); } catch (error) { console.error('[PromptPoolUI] 保存设置失败:', error); } } /** * 初始化事件监听器 */ initializeEventListeners() { // 提示词模式切换 document.querySelectorAll('input[name="promptMode"]').forEach(radio => { radio.addEventListener('change', () => { this.handlePromptModeChange(); this.saveSettings(); // 保存设置 }); }); // 提示词池内部步骤 Tabs(已移除) // 生成变体按钮 const generateBtn = document.getElementById('generateVariationsBtn'); if (generateBtn) { generateBtn.addEventListener('click', () => this.generateVariations()); } // 生成器元提示词面板开关 const toggleGenBtn = document.getElementById('toggleGeneratorPromptsBtn'); if (toggleGenBtn) { toggleGenBtn.addEventListener('click', () => { const panel = document.getElementById('generatorPromptsPanel'); if (panel) { panel.classList.toggle('hidden'); // 首次展开时,如未填写,则填入默认生成器提示词 if (!panel.classList.contains('hidden')) { const sysEl = document.getElementById('generatorSystemPrompt'); const userEl = document.getElementById('generatorUserPrompt'); if (sysEl && !sysEl.value) sysEl.value = this.buildDefaultGeneratorSystemPrompt(); if (userEl && !userEl.value) userEl.value = this.buildDefaultGeneratorUserPrompt(); } } }); } // 重置为默认 const resetGenBtn = document.getElementById('resetGeneratorPromptsBtn'); if (resetGenBtn) { resetGenBtn.addEventListener('click', () => { const sysEl = document.getElementById('generatorSystemPrompt'); const userEl = document.getElementById('generatorUserPrompt'); if (sysEl) sysEl.value = this.buildDefaultGeneratorSystemPrompt(); if (userEl) userEl.value = this.buildDefaultGeneratorUserPrompt(); this.saveSettings(); this.showNotification('已重置为默认生成器提示词', 'info'); const panel = document.getElementById('generatorPromptsPanel'); if (panel && panel.classList.contains('hidden')) panel.classList.remove('hidden'); }); } // 清空池按钮 const clearBtn = document.getElementById('clearPoolBtn'); if (clearBtn) { clearBtn.addEventListener('click', () => this.clearPool()); } // 导入导出按钮 const importBtn = document.getElementById('importPoolBtn'); const exportBtn = document.getElementById('exportPoolBtn'); if (importBtn) importBtn.addEventListener('click', () => this.importPool()); if (exportBtn) exportBtn.addEventListener('click', () => this.exportPool()); // 健康设置按钮 const healthSettingsBtn = document.getElementById('healthSettingsBtn'); if (healthSettingsBtn) { healthSettingsBtn.addEventListener('click', () => this.openHealthSettings()); } // 监听参数变化并保存设置 const parameterInputs = [ 'referenceSystemPrompt', 'referenceUserPrompt', 'variationCount', 'similarityControl', 'generationModel', 'promptPoolMode', 'generatorSystemPrompt', 'generatorUserPrompt', 'generationConcurrency', 'generatorLanguage' ]; parameterInputs.forEach(inputId => { const element = document.getElementById(inputId); if (element) { const eventType = element.type === 'textarea' ? 'input' : 'change'; element.addEventListener(eventType, () => { // 延迟保存,避免频繁写入 clearTimeout(this.saveTimeout); this.saveTimeout = setTimeout(() => { this.saveSettings(); }, 500); }); } }); // 初始化模式切换和可用模型列表 this.handlePromptModeChange(); this.populateAvailableModels(); this.updateHealthOverview(); // 定期更新健康状态显示 setInterval(() => { this.updateHealthOverview(); }, 30000); // 30秒更新一次 } // ===== 生成器提示词默认构造 ===== getSimilarityDescriptionUI(similarity) { const s = parseFloat(similarity || '0.6'); if (s <= 0.3) return '改写幅度很大(语序与措辞差异显著)'; if (s <= 0.5) return '改写幅度中等(保持等价约束,表达有明显变化)'; if (s <= 0.7) return '改写幅度适中(整体表达有所变化)'; return '改写幅度较小(细微措辞/顺序调整)'; } buildDefaultGeneratorSystemPrompt() { // 默认使用 ${count} 占位符,实际生成时再替换 return ( `你是资深提示词工程师,负责在不改变风格与约束的前提下,生成同风格的改写变体。 任务:基于参考提示词,生成 \${count} 个“同风格、同约束、同输出要求”的翻译提示词变体;仅在措辞、语序、句式、连接词与段落组织上做改写,以提升稳定性与一致性。 强制要求: 1. 返回 JSON,包含字段 variations(数组)。 2. 每个变体包含:name(名称)、systemPrompt(系统提示)、userPromptTemplate(用户提示模板)、description(简要说明)。必须同时给出 systemPrompt 与 userPromptTemplate 两个字段。 3. 相似度控制:请按请求给定的相似度要求理解(无需在文本中体现具体数值)。注意:风格与约束必须完全一致,仅体现措辞与语序差异。 4. userPromptTemplate 必须且仅出现一次占位符:\${targetLangName} 与 \${content}。 5. 严禁改变参考提示词的风格、语气、规则、术语偏好与输出格式要求;严禁引入“学术/商务/口语/文学”等风格标签的切换或暗示。 6. 允许调整表述顺序、同义替换与句式变化,但要保持语义与约束等价。 7. 仅输出严格的 JSON 对象(不包含 Markdown 代码块、注释或额外文本)。 8. 使用单行(minified)JSON 输出:不得换行、不得缩进。 9. 所有可读文本(如 name/description)请使用 \${genlanguage} 编写。` ); } buildDefaultGeneratorUserPrompt() { const sysRef = document.getElementById('referenceSystemPrompt')?.value || ''; const userRef = document.getElementById('referenceUserPrompt')?.value || ''; return ( `参考提示词(须保持风格/规则/输出要求一致): **系统提示:** ${sysRef} **用户提示模板:** ${userRef} 请基于以上参考提示词生成 \${count} 个“同风格改写”变体:保持风格、语气、规则与输出要求不变,仅做措辞/语序/句式的等价改写。 每个变体必须包含 systemPrompt 与 userPromptTemplate 两个字段;并且确保 userPromptTemplate 都包含且仅包含一次 \${targetLangName} 与 \${content} 占位符。严格输出为单行 JSON(无任何额外文字/提示/代码块)。 请使用 \${genlanguage} 编写所有需要人类阅读的文本(如 name/description)。` ); } /** * 填充可用的AI模型列表(使用当前源站点的模型列表) */ populateAvailableModels() { const modelSelect = document.getElementById('generationModel'); if (!modelSelect) return; // 保存当前选择 const currentSelection = modelSelect.value; // 清空现有选项 modelSelect.innerHTML = ''; // 填充模型列表 try { let hasAvailableModels = false; // 1. 添加预设模型选项 const predefinedModels = [ { value: 'mistral', name: 'Mistral Large' }, { value: 'deepseek', name: 'DeepSeek V3' }, { value: 'gemini', name: 'Gemini 2.0' } ]; predefinedModels.forEach(model => { const keys = typeof loadModelKeys === 'function' ? loadModelKeys(model.value) : []; const validKeys = keys.filter(key => key.status === 'valid' || key.status === 'untested' || !key.status); if (validKeys.length > 0) { const option = document.createElement('option'); option.value = model.value; option.textContent = model.name; modelSelect.appendChild(option); hasAvailableModels = true; } }); // 2. 获取当前选中的自定义源站点及其模型列表 if (typeof loadAllCustomSourceSites === 'function') { const allSites = loadAllCustomSourceSites(); // 获取当前选中的源站点ID(从设置中读取) let settings = {}; if (typeof loadSettings === 'function') { settings = loadSettings(); } else { try { settings = JSON.parse(localStorage.getItem('paperBurnerSettings') || '{}'); } catch (e) { settings = {}; } } const currentSiteId = settings.selectedCustomSourceSiteId; if (currentSiteId && allSites[currentSiteId]) { const currentSite = allSites[currentSiteId]; // 检查这个源站点是否有可用的API密钥 const customModelKey = `custom_source_${currentSiteId}`; const keys = typeof loadModelKeys === 'function' ? loadModelKeys(customModelKey) : []; const validKeys = keys.filter(key => key.status === 'valid' || key.status === 'untested' || !key.status); if (validKeys.length > 0) { console.log(`[PromptPoolUI] 源站点 ${currentSiteId} 有可用密钥`); // 如果源站点有可用模型列表,添加所有模型 if (currentSite.availableModels && currentSite.availableModels.length > 0) { console.log(`[PromptPoolUI] 添加源站点的 ${currentSite.availableModels.length} 个可用模型:`, currentSite.availableModels); currentSite.availableModels.forEach(model => { const option = document.createElement('option'); option.value = `${currentSiteId}:${model.id}`; // 使用站点ID:模型ID格式 option.textContent = `${model.name || model.id} (${currentSite.displayName || '自定义'})`; modelSelect.appendChild(option); hasAvailableModels = true; //console.log(`[PromptPoolUI] 已添加模型选项: ${option.textContent} (value: ${option.value})`); }); } else if (currentSite.modelId) { // 如果没有模型列表但有默认模型ID,添加默认模型 console.log(`[PromptPoolUI] 添加源站点的默认模型: ${currentSite.modelId}`); const option = document.createElement('option'); option.value = `${currentSiteId}:${currentSite.modelId}`; option.textContent = `${currentSite.modelId} (${currentSite.displayName || '自定义'})`; modelSelect.appendChild(option); hasAvailableModels = true; console.log(`[PromptPoolUI] 已添加默认模型选项: ${option.textContent} (value: ${option.value})`); } else { console.log(`[PromptPoolUI] 源站点 ${currentSiteId} 既没有availableModels也没有modelId`); } } else { console.warn(`[PromptPoolUI] 源站点 ${currentSiteId} 没有可用密钥。密钥检查结果:`, { keys: keys, validKeys: validKeys }); } } else { console.log('[PromptPoolUI] 没有选中的源站点或源站点不存在。', { currentSiteId: currentSiteId, availableSites: Object.keys(allSites) }); } } else { console.error('[PromptPoolUI] loadAllCustomSourceSites函数不可用'); } // 如果没有可用模型,添加提示 if (!hasAvailableModels) { const option = document.createElement('option'); option.value = ''; option.textContent = '请先在模型管理中配置API密钥和源站点'; option.disabled = true; modelSelect.appendChild(option); } // 恢复之前的选择(如果还存在) if (currentSelection && [...modelSelect.options].some(opt => opt.value === currentSelection)) { modelSelect.value = currentSelection; } console.log(`[PromptPoolUI] 模型列表填充完成,共 ${modelSelect.options.length} 个选项`); } catch (error) { console.error('填充可用模型列表失败:', error); const errorOption = document.createElement('option'); errorOption.value = ""; errorOption.textContent = "加载模型列表失败"; modelSelect.appendChild(errorOption); modelSelect.disabled = true; } } /** * 检查指定模型是否有可用的API密钥(简化版本,照抄ui.js) */ hasAvailableKeys(modelName) { console.log(`[PromptPoolUI] 检查模型 ${modelName} 的密钥可用性`); const keys = typeof loadModelKeys === 'function' ? loadModelKeys(modelName) : []; const validKeys = keys.filter(key => key.status === 'valid' || key.status === 'untested' || !key.status); const hasKeys = validKeys.length > 0; console.log(`[PromptPoolUI] 模型 ${modelName} 密钥检查结果: ${hasKeys}`, keys); return hasKeys; } /** * 处理提示词模式切换 */ handlePromptModeChange() { const selectedMode = document.querySelector('input[name="promptMode"]:checked')?.value; // 隐藏所有容器 document.getElementById('customPromptsContainer')?.classList.add('hidden'); document.getElementById('promptPoolContainer')?.classList.add('hidden'); // 显示对应容器 if (selectedMode === 'custom') { document.getElementById('customPromptsContainer')?.classList.remove('hidden'); } else if (selectedMode === 'pool') { document.getElementById('promptPoolContainer')?.classList.remove('hidden'); this.updateUI(); } // 更新Tabs样式 const tabMap = { builtin: document.getElementById('tabBuiltin'), custom: document.getElementById('tabCustom'), pool: document.getElementById('tabPool') }; Object.entries(tabMap).forEach(([mode, el]) => { if (!el) return; if (mode === selectedMode) { el.classList.remove('text-gray-500','border-transparent','hover:text-gray-700','hover:border-gray-300'); el.classList.add('text-blue-600','border-blue-600'); } else { el.classList.remove('text-blue-600','border-blue-600'); el.classList.add('text-gray-500','border-transparent','hover:text-gray-700','hover:border-gray-300'); } }); } /** * 生成提示词变体 */ async generateVariations() { const generateBtn = document.getElementById('generateVariationsBtn'); const generateStatus = document.getElementById('generateStatus'); if (!generateBtn || !generateStatus) return; // 获取参数 const referenceSystemPrompt = document.getElementById('referenceSystemPrompt')?.value?.trim(); const referenceUserPrompt = document.getElementById('referenceUserPrompt')?.value?.trim(); const count = parseInt(document.getElementById('variationCount')?.value || '10'); const similarity = parseFloat(document.getElementById('similarityControl')?.value || '0.6'); const apiModel = document.getElementById('generationModel')?.value; const concurrencyRaw = document.getElementById('generationConcurrency')?.value || '1'; let concurrency = parseInt(concurrencyRaw, 10); if (!Number.isFinite(concurrency) || concurrency < 1) concurrency = 1; if (concurrency > 10) concurrency = 10; const genLanguage = (document.getElementById('generatorLanguage')?.value || '中文').trim() || '中文'; // 验证参数 if (!referenceSystemPrompt || !referenceUserPrompt) { this.showNotification('请先填写参考提示词', 'warning'); return; } if (!apiModel) { this.showNotification('请选择AI模型', 'warning'); return; } // 验证占位符 const targetLangCount = (referenceUserPrompt.match(/\$\{targetLangName\}/g) || []).length; const contentCount = (referenceUserPrompt.match(/\$\{content\}/g) || []).length; if (targetLangCount !== 1 || contentCount !== 1) { this.showNotification('参考用户提示词必须包含且仅包含一次 ${targetLangName} 和 ${content} 占位符', 'error'); return; } // 获取API密钥 let apiKey; try { apiKey = await this.getApiKeyForModel(apiModel); if (!apiKey) { this.showNotification(`模型 ${apiModel} 没有可用的API密钥,请先在模型管理中配置`, 'error'); return; } } catch (error) { this.showNotification(`获取API密钥失败: ${error.message}`, 'error'); return; } // 显示生成状态 generateBtn.disabled = true; generateStatus.classList.remove('hidden'); this.showGenerationProgress(`准备生成:${count} × 并发 ${concurrency},共计 ${count*concurrency} 个`, 10); try { // 支持在生成器提示词中使用 ${count} 与 ${genlanguage} 占位符 let genSysRaw = document.getElementById('generatorSystemPrompt')?.value?.trim(); let genUserRaw = document.getElementById('generatorUserPrompt')?.value?.trim(); if (!genSysRaw) genSysRaw = this.buildDefaultGeneratorSystemPrompt(); if (!genUserRaw) genUserRaw = this.buildDefaultGeneratorUserPrompt(); const genSys = genSysRaw .replace(/\$\{count\}/g, String(count)) .replace(/\$\{genlanguage\}/g, genLanguage); const genUser = genUserRaw .replace(/\$\{count\}/g, String(count)) .replace(/\$\{genlanguage\}/g, genLanguage); // 构造并发任务 const tasks = []; let finished = 0; let successBatches = 0; let failedBatches = 0; const collected = []; const totalBatches = concurrency; const updateBatchProgress = () => { finished++; const pct = 30 + Math.round((finished / totalBatches) * 50); // 30%~80% 区间 this.showGenerationProgress(`AI并发生成中... 已完成 ${finished}/${totalBatches} 次`, pct); }; for (let i = 0; i < totalBatches; i++) { tasks.push( (async () => { // 轻微抖动,降低同秒触发限流概率 await new Promise(r => setTimeout(r, 80 * i)); let success = false; let lastErr = null; const maxAttempts = 2; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { const arr = await this.promptPool.generateVariationsWithAI( referenceSystemPrompt, referenceUserPrompt, count, similarity, apiModel, apiKey, { generatorSystemPrompt: genSys, generatorUserPrompt: genUser } ); collected.push(...arr); successBatches++; success = true; break; } catch (err) { lastErr = err; const msg = (err && err.message) ? err.message : ''; if (attempt < maxAttempts && /429|rate|Too Many|limit/i.test(msg)) { // 指数退避 + 抖动 const delay = 400 + Math.floor(Math.random()*400) * attempt; await new Promise(r => setTimeout(r, delay)); continue; } break; } } if (!success) { console.warn('[PromptPoolUI] 并发批次失败:', lastErr); failedBatches++; } updateBatchProgress(); })() ); } await Promise.allSettled(tasks); // 去重:按 systemPrompt + userPromptTemplate 组合键 const seen = new Set(); const unique = []; for (const v of collected) { const key = `${(v.systemPrompt||'').trim()}||${(v.userPromptTemplate||'').trim()}`; if (!seen.has(key)) { seen.add(key); unique.push(v); } } this.showGenerationProgress('验证并写入结果...', 90); // 添加到池中 this.promptPool.addVariationsToPool(unique); // 更新UI this.updateUI(); this.showGenerationProgress('完成!', 100); // 成功/失败反馈 const totalExpected = count * totalBatches; const msg = `成功生成 ${unique.length}/${totalExpected} 条;批次成功 ${successBatches},失败 ${failedBatches}`; this.showNotification(msg, failedBatches === 0 ? 'success' : 'warning'); } catch (error) { console.error('AI生成提示词变体失败:', error); this.showNotification('生成提示词变体失败:' + error.message, 'error'); } finally { // 恢复按钮状态 generateBtn.disabled = false; generateStatus.classList.add('hidden'); this.hideGenerationProgress(); } } /** * 获取指定模型的API密钥(处理自定义源站点的新格式,添加详细调试) */ async getApiKeyForModel(apiModel) { try { console.log(`[PromptPoolUI] 获取模型 ${apiModel} 的API密钥`); let actualModelName = apiModel; // 处理自定义源站点的格式: "siteId:modelId" if (apiModel.includes(':')) { const separatorIndex = apiModel.indexOf(':'); const siteId = apiModel.slice(0, separatorIndex); actualModelName = `custom_source_${siteId}`; } // 先检查 loadModelKeys 函数是否可用 if (typeof loadModelKeys !== 'function') { console.error(`[PromptPoolUI] loadModelKeys函数不可用`); throw new Error('loadModelKeys函数不可用'); } // 调用 loadModelKeys 获取密钥 const keys = loadModelKeys(actualModelName); if (!keys) throw new Error('loadModelKeys返回null/undefined'); if (!Array.isArray(keys)) throw new Error('loadModelKeys返回的不是数组'); if (keys.length > 0) { // 优先选择已验证有效的密钥 const validKeys = keys.filter(key => key.status === 'valid'); if (validKeys.length > 0) { return validKeys[0].value; // 应该是 .value 而不是 .key } // 如果没有已验证的,选择未测试的 const untestedKeys = keys.filter(key => key.status === 'untested' || !key.status); if (untestedKeys.length > 0) { return untestedKeys[0].value; // 应该是 .value 而不是 .key } // 如果都没有,返回第一个 return keys[0].value; // 应该是 .value 而不是 .key } throw new Error('未找到可用的API密钥'); } catch (error) { console.error(`[PromptPoolUI] 获取模型 ${apiModel} 的API密钥失败:`, error); throw error; } } /** * 显示生成进度 */ showGenerationProgress(message, percentage) { // 检查是否已存在进度条,如果没有则创建 let progressContainer = document.getElementById('promptGenerationProgress'); if (!progressContainer) { progressContainer = document.createElement('div'); progressContainer.id = 'promptGenerationProgress'; progressContainer.className = 'fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center p-4'; progressContainer.innerHTML = `
连续失败时自动失活提示词
失败时切换到其他健康提示词
失活一段时间后自动复活
替换队列中失败提示词的请求
超过此次数后提示词将被失活
失活后等待多久自动复活
智能健康管理说明:
正在加载提示词池...