// ==UserScript==
// @name 给DeepSeek添加原生化的提示词管理功能
// @namespace http://tampermonkey.net/
// @version 1.8.9
// @description 为 DeepSeek 添加提示词管理功能。全新Linear设计风格,TailwindCSS构建
// @author gpt
// @match https://chat.deepseek.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- Script Configuration ---
const SCRIPT_PREFIX = 'dspm_'; // Prefix for localStorage keys, IDs, and CSS classes
// --- Helper Functions ---
const $ = (selector, parent = document) => parent.querySelector(selector);
const $$ = (selector, parent = document) => parent.querySelectorAll(selector);
const debounce = (fn, delay) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
// --- Dynamic CDN Injection ---
function injectCDN(url, type) {
return new Promise((resolve, reject) => {
let element;
if (type === 'script') {
element = document.createElement('script');
element.src = url;
} else if (type === 'link') {
element = document.createElement('link');
element.href = url;
element.rel = 'stylesheet';
}
if (element) {
element.onload = resolve;
element.onerror = (err) => {
console.error(`DSPM: Failed to load CDN ${type}: ${url}`, err);
reject(err);
};
document.head.appendChild(element);
} else {
reject(new Error('Invalid CDN type'));
}
});
}
// --- Theme Management ---
const applyTheme = (theme) => {
console.log(`DSPM: Applying theme: ${theme}`);
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
GM_setValue(SCRIPT_PREFIX + 'theme', theme);
};
const getSavedTheme = () => GM_getValue(SCRIPT_PREFIX + 'theme', 'system');
const initTheme = () => {
const savedTheme = getSavedTheme();
console.log(`DSPM: Initializing theme. Saved: ${savedTheme}`);
if (savedTheme === 'system') {
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
console.log(`DSPM: System prefers dark: ${systemPrefersDark}`);
applyTheme(systemPrefersDark ? 'dark' : 'light');
} else {
applyTheme(savedTheme);
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
console.log(`DSPM: System theme changed. Prefers dark: ${e.matches}`);
if (getSavedTheme() === 'system') {
applyTheme(e.matches ? 'dark' : 'light');
}
});
};
// --- Base Styles (Minimal, mostly for scrollbars and transitions) ---
GM_addStyle(`
.dspm-smooth-scroll { scroll-behavior: smooth; }
.dspm-custom-scrollbar::-webkit-scrollbar { width: 8px; height: 8px; }
.dspm-custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.dspm-custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5); /* Light mode thumb color */
border-radius: 4px;
}
.dspm-custom-scrollbar::-webkit-scrollbar-thumb:hover { background-color: rgba(107, 114, 128, 0.7); }
.dark .dspm-custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(75, 85, 99, 0.6); /* Dark mode thumb color */
}
.dark .dspm-custom-scrollbar::-webkit-scrollbar-thumb:hover { background-color: rgba(55, 65, 81, 0.8); }
.dspm-fade-in { animation: dspmFadeInAnimation 0.3s ease-out forwards; opacity: 0; }
@keyframes dspmFadeInAnimation { to { opacity: 1; } }
/* Management Panel Styles */
.dspm-management-side-panel {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateX(100%);
height: 100vh !important; /* Ensure panel is full viewport height */
}
.dspm-management-side-panel.visible { transform: translateX(0); }
/* Content area within the management panel */
.dspm-management-content-area {
height: calc(100vh - 62px) !important; /* Full height minus header (assuming header is 62px) */
overflow-y: scroll !important; /* Ensure vertical scrollbar track is always visible */
}
.dspm-management-panel-toggle-btn { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.2s, color 0.2s; }
.dspm-management-panel-toggle-btn.panel-visible { transform: translateX(-340px); /* Adjust if panel width changes */ }
body { transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out; }
`);
// --- UI Element Variables ---
let usagePromptDropdown;
let promptUsageButtonWrapper;
let promptUsageButton;
let managementSidePanel;
let managementPanelToggleButton;
let themeToggleButton;
// --- Core Functions ---
function createElements() {
const buttonContainer = $('.ec4f5d61');
if (!buttonContainer || $(`#${SCRIPT_PREFIX}usage_button`)) return;
promptUsageButtonWrapper = document.createElement('div');
promptUsageButtonWrapper.className = 'prompt-usage-button-wrapper inline-flex items-center relative';
promptUsageButton = document.createElement('button');
promptUsageButton.id = SCRIPT_PREFIX + 'usage_button';
promptUsageButton.className = `prompt-usage-button flex items-center justify-center h-[34px] px-[14px] py-[8px] text-[14px] font-medium bg-white dark:bg-gray-800 text-[#4c4c4c] dark:text-gray-300 border border-[rgba(0,0,0,0.12)] dark:border-gray-600 rounded-[10px] hover:bg-[#E0E4ED] dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800 transition-all duration-150 ease-in-out transform hover:scale-105 ml-1 mr-2.5`;
promptUsageButton.innerHTML = `<i class="fas fa-star w-[18px] h-[18px] mr-2"></i><span class="button-text-content">提示词库</span>`;
promptUsageButtonWrapper.appendChild(promptUsageButton);
const buttonsInContainer = buttonContainer.children;
if (buttonsInContainer.length >= 1) buttonContainer.insertBefore(promptUsageButtonWrapper, buttonsInContainer[1]);
else buttonContainer.appendChild(promptUsageButtonWrapper);
usagePromptDropdown = document.createElement('div');
usagePromptDropdown.id = SCRIPT_PREFIX + 'usage_dropdown';
usagePromptDropdown.className = `usage-prompt-dropdown dspm-custom-scrollbar dspm-fade-in hidden absolute z-50 mt-2 w-72 md:w-80 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-xl max-h-80 overflow-y-auto p-2 space-y-1`; // Dropdown uses overflow-y: auto
document.body.appendChild(usagePromptDropdown);
promptUsageButton.addEventListener('click', (e) => {
e.stopPropagation();
const isVisible = !usagePromptDropdown.classList.contains('hidden');
if (isVisible) closeUsageDropdown();
else openUsageDropdown();
});
managementPanelToggleButton = document.createElement('button');
managementPanelToggleButton.id = SCRIPT_PREFIX + 'management_toggle_btn';
managementPanelToggleButton.className = `dspm-management-panel-toggle-btn fixed top-1/2 -translate-y-1/2 right-0 z-40 p-2.5 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white border border-r-0 border-gray-300 dark:border-gray-600 rounded-l-md shadow-md focus:outline-none transition-all duration-150 ease-in-out transform hover:scale-105`;
managementPanelToggleButton.title = '管理提示词';
managementPanelToggleButton.innerHTML = `<i class="fas fa-cog fa-lg"></i>`;
document.body.appendChild(managementPanelToggleButton);
managementPanelToggleButton.addEventListener('click', (e) => { e.stopPropagation(); toggleManagementPanel(); });
managementSidePanel = document.createElement('div');
managementSidePanel.id = SCRIPT_PREFIX + 'management_side_panel';
managementSidePanel.className = `dspm-management-side-panel fixed top-0 right-0 h-full w-full max-w-sm md:max-w-md z-50 bg-gray-50 dark:bg-gray-850 shadow-2xl flex flex-col border-l border-gray-200 dark:border-gray-700`;
document.body.appendChild(managementSidePanel);
document.addEventListener('click', (e) => {
if (document.body.classList.contains(SCRIPT_PREFIX + 'modal-open')) {
// If a modal is open, its own overlay click handler should manage closure.
// This global listener should not interfere with modal overlay clicks.
// The check e.target.id for overlays was removed here as it's redundant
// with the modal's own specific overlay click handlers.
return;
}
if (usagePromptDropdown && !usagePromptDropdown.classList.contains('hidden')) {
if (!usagePromptDropdown.contains(e.target) && !promptUsageButton.contains(e.target)) {
closeUsageDropdown();
}
}
if (managementSidePanel && managementSidePanel.classList.contains('visible')) {
const importExportMenu = $(`#${SCRIPT_PREFIX}import_export_menu`);
if (!managementSidePanel.contains(e.target) && !managementPanelToggleButton.contains(e.target) && !(importExportMenu && importExportMenu.contains(e.target))) {
toggleManagementPanel(false);
}
}
});
}
function openUsageDropdown() {
if (!usagePromptDropdown || !promptUsageButton) return;
updateUsageDropdown();
usagePromptDropdown.classList.remove('hidden');
promptUsageButton.classList.add('active', 'bg-[#E0E4ED]', 'dark:bg-gray-700');
updateUsageDropdownPosition();
}
function closeUsageDropdown() {
if (!usagePromptDropdown || !promptUsageButton) return;
usagePromptDropdown.classList.add('hidden');
promptUsageButton.classList.remove('active', 'bg-[#E0E4ED]', 'dark:bg-gray-700');
}
function updateUsageDropdownPosition() {
if (!usagePromptDropdown || !promptUsageButtonWrapper) return;
const buttonRect = promptUsageButtonWrapper.getBoundingClientRect();
const dropdownHeight = usagePromptDropdown.offsetHeight;
const windowHeight = window.innerHeight;
let top = buttonRect.bottom + 8;
if (top + dropdownHeight > windowHeight - 20) top = buttonRect.top - dropdownHeight - 8;
if (top < 20) top = 20;
usagePromptDropdown.style.left = `${buttonRect.left}px`;
usagePromptDropdown.style.top = `${top}px`;
usagePromptDropdown.style.minWidth = `${buttonRect.width}px`;
}
function updateUsageDropdown() {
if (!usagePromptDropdown) return;
const savedPrompts = GM_getValue(SCRIPT_PREFIX + 'prompts', []);
usagePromptDropdown.innerHTML = '';
if (savedPrompts.length === 0) {
usagePromptDropdown.innerHTML = `<div class="p-4 text-center text-sm text-gray-500 dark:text-gray-400">暂无提示词。请通过右侧 <i class="fas fa-cog mx-1"></i> 管理面板添加。</div>`;
return;
}
savedPrompts.forEach(prompt => {
const item = document.createElement('div');
item.className = `p-2.5 rounded-md cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-150`;
item.innerHTML = `<div class="font-semibold text-sm text-gray-800 dark:text-gray-200 truncate">${prompt.title}</div><div class="text-xs text-gray-500 dark:text-gray-400 truncate mt-0.5">${prompt.content}</div>`;
item.addEventListener('click', async () => {
try {
const textArea = $('#chat-input');
if (!textArea) throw new Error('Chat input not found');
textArea.focus();
document.execCommand('selectAll', false, null);
document.execCommand('delete', false, null);
document.execCommand('insertText', false, prompt.content);
closeUsageDropdown();
} catch (err) { console.error('DSPM: Failed to fill prompt:', err); alert('填充提示词失败。'); }
});
usagePromptDropdown.appendChild(item);
});
}
function toggleManagementPanel(forceState) {
if (!managementSidePanel || !managementPanelToggleButton) return;
const show = forceState !== undefined ? forceState : !managementSidePanel.classList.contains('visible');
if (show) {
updateManagementPanelContent();
managementSidePanel.classList.add('visible');
managementPanelToggleButton.classList.add('panel-visible');
} else {
managementSidePanel.classList.remove('visible');
managementPanelToggleButton.classList.remove('panel-visible');
}
}
function updateManagementPanelContent() {
if (!managementSidePanel) return;
const savedPrompts = GM_getValue(SCRIPT_PREFIX + 'prompts', []);
const currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
managementSidePanel.innerHTML = `
<div class="flex flex-col h-full">
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 sticky top-0 z-10">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">管理提示词</h3>
<div class="flex items-center space-x-1">
<button id="${SCRIPT_PREFIX}theme_toggle_btn" title="切换主题" class="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200 transition-colors">
<i class="fas ${currentTheme === 'dark' ? 'fa-sun' : 'fa-moon'} fa-fw"></i>
</button>
<button id="${SCRIPT_PREFIX}import_export_btn" title="导入/导出" class="p-2 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200 transition-colors">
<i class="fas fa-file-import fa-fw"></i>
</button>
<button id="${SCRIPT_PREFIX}clear_all_btn" title="清空全部提示词" class="p-2 rounded-md text-red-500 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-700/30 hover:text-red-700 dark:hover:text-red-300 transition-colors">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
<button id="${SCRIPT_PREFIX}add_prompt_btn" class="px-3 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800 transition-all duration-150 ease-in-out transform hover:scale-105">
<i class="fas fa-plus mr-1.5"></i> 创建
</button>
</div>
</div>
<div class="dspm-management-content-area flex-grow p-4 dspm-custom-scrollbar dspm-smooth-scroll space-y-3">
${savedPrompts.length === 0 ?
`<div class="text-center py-10">
<i class="fas fa-folder-open fa-3x text-gray-400 dark:text-gray-500 mb-3"></i>
<p class="text-sm text-gray-500 dark:text-gray-400">暂无提示词,点击“创建”添加一个吧!</p>
</div>` :
savedPrompts.map((prompt, index) => `
<div id="${SCRIPT_PREFIX}prompt_card_${index}" class="dspm-prompt-card bg-white dark:bg-gray-750 p-3.5 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md hover:border-gray-300 dark:hover:border-gray-600 transition-all duration-150 ease-in-out">
<div class="flex items-center justify-between">
<h4 class="text-md font-semibold text-gray-800 dark:text-gray-100 truncate pr-2 flex-grow">${prompt.title}</h4>
<button data-index="${index}" class="dspm-edit-prompt-btn p-1.5 rounded-md text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">
<i class="fas fa-pen fa-sm"></i>
</button>
</div>
<p class="text-sm text-gray-600 dark:text-gray-300 mt-1.5 break-words line-clamp-2">${prompt.content}</p>
</div>`).join('')
}
</div>
</div>`;
themeToggleButton = $(`#${SCRIPT_PREFIX}theme_toggle_btn`, managementSidePanel);
themeToggleButton.addEventListener('click', (e) => {
e.stopPropagation();
console.log('DSPM: Theme toggle clicked');
const newTheme = document.documentElement.classList.contains('dark') ? 'light' : 'dark';
applyTheme(newTheme);
themeToggleButton.innerHTML = `<i class="fas ${newTheme === 'dark' ? 'fa-sun' : 'fa-moon'} fa-fw"></i>`;
});
$(`#${SCRIPT_PREFIX}add_prompt_btn`, managementSidePanel).addEventListener('click', (e) => { e.stopPropagation(); showEditPromptModal(); });
$$('.dspm-edit-prompt-btn', managementSidePanel).forEach(button => {
button.addEventListener('click', function(e) { e.stopPropagation(); showEditPromptModal(savedPrompts[parseInt(this.dataset.index)], parseInt(this.dataset.index)); });
});
setupImportExportMenu($(`#${SCRIPT_PREFIX}import_export_btn`, managementSidePanel));
const clearAllBtn = $(`#${SCRIPT_PREFIX}clear_all_btn`, managementSidePanel);
if (clearAllBtn) {
clearAllBtn.addEventListener('click', (e) => {
e.stopPropagation();
showConfirmationDialog(
'确认清空全部提示词',
'确定要删除所有已保存的提示词吗?此操作无法撤销。',
() => {
console.log('DSPM: Clearing all prompts.');
GM_setValue(SCRIPT_PREFIX + 'prompts', []);
updateManagementPanelContent();
updateUsageDropdown();
alert('所有提示词已清空!');
},
'清空全部'
);
});
}
}
function showEditPromptModal(prompt = null, index = null) {
const isEditing = prompt !== null;
const modalId = SCRIPT_PREFIX + 'edit_modal_overlay';
if ($(`#${modalId}`)) $(`#${modalId}`).remove();
const modalOverlay = document.createElement('div');
modalOverlay.id = modalId;
modalOverlay.className = `fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gray-900 bg-opacity-60 dark:bg-opacity-75 dspm-fade-in`;
modalOverlay.innerHTML = `<div class="bg-white dark:bg-gray-800 bg-opacity-100 dark:bg-opacity-100 rounded-lg shadow-xl w-full max-w-lg p-6 space-y-4 transform transition-all duration-300 ease-out scale-95 opacity-0" role="dialog" aria-modal="true"> <div class="flex items-center justify-between pb-3 border-b border-gray-200 dark:border-gray-700"><h3 class="text-xl font-semibold text-gray-900 dark:text-white">${isEditing ? '编辑指令' : '创建新指令'}</h3>${isEditing ? `<button id="${SCRIPT_PREFIX}delete_prompt_btn" class="px-3 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/50 hover:bg-red-200 dark:hover:bg-red-800/70 rounded-md transition-colors"><i class="fas fa-trash-alt mr-1"></i> 删除</button>` : ''}<button id="${SCRIPT_PREFIX}close_modal_btn" class="p-1.5 rounded-md text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"><i class="fas fa-times fa-lg"></i></button></div> <div class="space-y-4"><div><label for="${SCRIPT_PREFIX}prompt_title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">指令标题 <span class="text-red-500">*</span></label><input type="text" id="${SCRIPT_PREFIX}prompt_title" value="${isEditing ? prompt.title : ''}" placeholder="例如:周报小助手" class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"></div><div><label for="${SCRIPT_PREFIX}prompt_content" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">指令内容 <span class="text-red-500">*</span></label><textarea id="${SCRIPT_PREFIX}prompt_content" rows="6" placeholder="请输入希望AI执行的具体指令内容..." class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dspm-custom-scrollbar">${isEditing ? prompt.content : ''}</textarea></div></div> <div class="pt-4 flex justify-end space-x-3 border-t border-gray-200 dark:border-gray-700"><button id="${SCRIPT_PREFIX}cancel_modal_btn" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">取消</button><button id="${SCRIPT_PREFIX}save_prompt_btn" class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800 transition-all duration-150 ease-in-out transform hover:scale-105">${isEditing ? '保存修改' : '创建指令'}</button></div></div>`;
document.body.appendChild(modalOverlay);
document.body.classList.add(SCRIPT_PREFIX + 'modal-open');
setTimeout(() => { const modalContent = $('div[role="dialog"]', modalOverlay); if (modalContent) { modalContent.classList.remove('scale-95', 'opacity-0'); modalContent.classList.add('scale-100', 'opacity-100'); } }, 10);
const closeModal = () => {
const modalContent = $('div[role="dialog"]', modalOverlay);
if (modalContent) {
modalContent.classList.add('scale-95', 'opacity-0');
modalContent.classList.remove('scale-100', 'opacity-100');
setTimeout(() => {
if (modalOverlay) modalOverlay.remove(); // Check if modalOverlay still exists
document.body.classList.remove(SCRIPT_PREFIX + 'modal-open');
}, 300);
} else {
if (modalOverlay) modalOverlay.remove(); // Check if modalOverlay still exists
document.body.classList.remove(SCRIPT_PREFIX + 'modal-open');
}
};
$(`#${SCRIPT_PREFIX}close_modal_btn`, modalOverlay).onclick = closeModal;
$(`#${SCRIPT_PREFIX}cancel_modal_btn`, modalOverlay).onclick = closeModal;
// Diagnostic log for edit/create modal overlay click
modalOverlay.onclick = (e) => {
console.log('DSPM: Edit Modal Overlay clicked. Target:', e.target, 'Expected Overlay:', modalOverlay);
if (e.target === modalOverlay) {
console.log('DSPM: Closing Edit Modal due to overlay click.');
closeModal();
} else {
console.log('DSPM: Edit Modal Overlay click ignored, target was not overlay itself.');
}
};
if (!isEditing && $('#chat-input')?.value.trim()) $(`#${SCRIPT_PREFIX}prompt_content`, modalOverlay).value = $('#chat-input').value.trim();
$(`#${SCRIPT_PREFIX}prompt_title`, modalOverlay).focus();
if (isEditing) {
$(`#${SCRIPT_PREFIX}delete_prompt_btn`, modalOverlay).onclick = () => {
showConfirmationDialog('确认删除', `确定要删除指令 “<strong>${prompt.title}</strong>” 吗?此操作无法撤销。`, () => {
const prompts = GM_getValue(SCRIPT_PREFIX + 'prompts', []);
prompts.splice(index, 1); GM_setValue(SCRIPT_PREFIX + 'prompts', prompts);
closeModal(); updateManagementPanelContent(); updateUsageDropdown();
});
};
}
$(`#${SCRIPT_PREFIX}save_prompt_btn`, modalOverlay).onclick = () => {
const title = $(`#${SCRIPT_PREFIX}prompt_title`, modalOverlay).value.trim();
const content = $(`#${SCRIPT_PREFIX}prompt_content`, modalOverlay).value.trim();
if (!title || !content) { alert(title ? '指令内容不能为空!' : '指令标题不能为空!'); (title ? $(`#${SCRIPT_PREFIX}prompt_content`, modalOverlay) : $(`#${SCRIPT_PREFIX}prompt_title`, modalOverlay)).focus(); return; }
const prompts = GM_getValue(SCRIPT_PREFIX + 'prompts', []);
if (isEditing) prompts[index] = { title, content }; else prompts.push({ title, content });
GM_setValue(SCRIPT_PREFIX + 'prompts', prompts);
closeModal(); updateManagementPanelContent(); updateUsageDropdown();
};
}
function showConfirmationDialog(title, message, onConfirm, confirmButtonText = '确认删除') {
const dialogId = SCRIPT_PREFIX + 'confirm_dialog_overlay';
if ($(`#${dialogId}`)) $(`#${dialogId}`).remove();
const dialogOverlay = document.createElement('div');
dialogOverlay.id = dialogId;
dialogOverlay.className = `fixed inset-0 z-[110] flex items-center justify-center p-4 bg-gray-900 bg-opacity-70 dark:bg-opacity-80 dspm-fade-in`;
dialogOverlay.innerHTML = `<div class="bg-white dark:bg-gray-800 bg-opacity-100 dark:bg-opacity-100 rounded-lg shadow-xl w-full max-w-md p-6 space-y-4 transform transition-all duration-300 ease-out scale-95 opacity-0"><h3 class="text-lg font-medium text-gray-900 dark:text-white">${title}</h3><p class="text-sm text-gray-600 dark:text-gray-300">${message}</p><div class="flex justify-end space-x-3 pt-3"><button id="${SCRIPT_PREFIX}confirm_cancel" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">取消</button><button id="${SCRIPT_PREFIX}confirm_ok" class="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 dark:focus:ring-offset-gray-800">${confirmButtonText}</button></div></div>`;
document.body.appendChild(dialogOverlay);
document.body.classList.add(SCRIPT_PREFIX + 'modal-open');
setTimeout(() => { const dialogContent = $('div > div', dialogOverlay); if(dialogContent) { dialogContent.classList.remove('scale-95', 'opacity-0'); dialogContent.classList.add('scale-100', 'opacity-100'); } }, 10);
const closeDialog = () => {
const dialogContent = $('div > div', dialogOverlay);
if(dialogContent) {
dialogContent.classList.add('scale-95', 'opacity-0');
dialogContent.classList.remove('scale-100', 'opacity-100');
}
setTimeout(() => {
if (dialogOverlay) dialogOverlay.remove(); // Check if dialogOverlay still exists
if (!$(`#${SCRIPT_PREFIX}edit_modal_overlay`)) {
document.body.classList.remove(SCRIPT_PREFIX + 'modal-open');
}
}, 300);
};
$(`#${SCRIPT_PREFIX}confirm_cancel`, dialogOverlay).onclick = closeDialog;
$(`#${SCRIPT_PREFIX}confirm_ok`, dialogOverlay).onclick = () => { onConfirm(); closeDialog(); };
// Diagnostic log for confirmation dialog overlay click
dialogOverlay.onclick = (e) => {
console.log('DSPM: Confirm Dialog Overlay clicked. Target:', e.target, 'Expected Overlay:', dialogOverlay);
if (e.target === dialogOverlay) {
console.log('DSPM: Closing Confirm Dialog due to overlay click.');
closeDialog();
} else {
console.log('DSPM: Confirm Dialog Overlay click ignored, target was not overlay itself.');
}
};
}
function setupImportExportMenu(buttonElement) {
if (!buttonElement) return;
const newBtn = buttonElement.cloneNode(true);
buttonElement.parentNode.replaceChild(newBtn, buttonElement);
buttonElement = newBtn;
buttonElement.addEventListener('click', (e) => {
e.stopPropagation();
const menuId = SCRIPT_PREFIX + 'import_export_menu';
const existingMenu = $(`#${menuId}`);
if (existingMenu) { existingMenu.remove(); return; }
const menu = document.createElement('div'); menu.id = menuId;
menu.className = `absolute z-[120] mt-1.5 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md shadow-lg py-1 dspm-fade-in`;
menu.innerHTML = `<button data-action="export" class="w-full text-left px-3 py-1.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors flex items-center"><i class="fas fa-file-export w-4 h-4 mr-2.5 text-gray-400 dark:text-gray-500"></i> 导出 JSON</button><button data-action="import" class="w-full text-left px-3 py-1.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white transition-colors flex items-center"><i class="fas fa-file-import w-4 h-4 mr-2.5 text-gray-400 dark:text-gray-500"></i> 导入 JSON</button>`;
document.body.appendChild(menu);
const btnRect = buttonElement.getBoundingClientRect();
menu.style.top = `${btnRect.bottom + 4}px`;
menu.style.left = `${btnRect.right - menu.offsetWidth}px`;
const closeMenu = () => menu.remove();
const clickOutsideHandler = (event) => {
if (!menu.contains(event.target) && !buttonElement.contains(event.target)) {
closeMenu();
document.removeEventListener('click', clickOutsideHandler, true);
}
};
document.addEventListener('click', clickOutsideHandler, true);
$$('button', menu).forEach(btn => {
btn.onclick = () => {
console.log(`DSPM: Import/Export action: ${btn.dataset.action}`);
if (btn.dataset.action === 'export') {
const prompts = GM_getValue(SCRIPT_PREFIX + 'prompts', []);
if (prompts.length === 0) { alert("没有可导出的提示词。"); closeMenu(); return; }
const blob = new Blob([JSON.stringify(prompts, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `deepseek-prompts-${new Date().toISOString().slice(0,10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('DSPM: Export initiated.');
} else if (btn.dataset.action === 'import') {
const input = document.createElement('input');
input.type = 'file'; input.accept = '.json';
input.onchange = (ev) => {
const file = ev.target.files[0];
if (file) {
console.log(`DSPM: File selected for import: ${file.name}`);
const reader = new FileReader();
reader.onload = (le) => {
try {
const imported = JSON.parse(le.target.result);
if (Array.isArray(imported) && imported.every(p => typeof p.title === 'string' && typeof p.content === 'string')) {
GM_setValue(SCRIPT_PREFIX + 'prompts', imported);
updateManagementPanelContent(); updateUsageDropdown();
alert('提示词导入成功!');
console.log('DSPM: Prompts imported successfully.');
} else { alert('导入失败:文件格式无效。'); console.error('DSPM: Invalid import file format.');}
} catch (err) { console.error("DSPM: Import error:", err); alert('导入失败:无法解析文件。'); }
};
reader.readAsText(file);
}
};
input.click();
console.log('DSPM: Import file dialog opened.');
}
closeMenu();
};
});
});
}
async function initScript() {
try {
console.log('DSPM: Initializing script...');
await injectCDN('https://cdn.tailwindcss.com/3.4.3', 'script');
console.log('DSPM: TailwindCSS CDN loaded.');
await injectCDN('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css', 'link');
console.log('DSPM: Font Awesome CDN loaded.');
initTheme();
createElements();
console.log('DSPM: Script initialized successfully.');
} catch (error) {
console.error('DSPM: Failed to initialize script or load resources.', error);
alert('提示词管理脚本加载资源失败,部分功能可能无法正常使用。请检查网络连接或浏览器控制台。');
}
}
const observer = new MutationObserver(debounce(() => {
if (!$(`#${SCRIPT_PREFIX}usage_button`) && $('.ec4f5d61')) {
console.log("DSPM: Detected button container via MutationObserver, attempting to create elements.");
createElements();
}
}, 500));
observer.observe(document.body, { childList: true, subtree: true });
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initScript);
} else {
initScript();
}
window.addEventListener('resize', debounce(() => {
if (usagePromptDropdown && !usagePromptDropdown.classList.contains('hidden')) {
updateUsageDropdownPosition();
}
const importExportMenu = $(`#${SCRIPT_PREFIX}import_export_menu`);
if (importExportMenu) {
const importExportBtn = $(`#${SCRIPT_PREFIX}import_export_btn`, managementSidePanel);
if (importExportBtn) {
const btnRect = importExportBtn.getBoundingClientRect();
importExportMenu.style.top = `${btnRect.bottom + 4}px`;
importExportMenu.style.left = `${btnRect.right - importExportMenu.offsetWidth}px`;
}
}
}, 150));
})();