<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>提示词设置</title>
<script src="javascript/tailwindcss.js"></script>
<script src="javascript/vue.global.js"></script>
<style>
[v-cloak] {
display: none;
}
/* 禁用数字输入框滚轮事件 */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
body,
html {
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}
#app {
height: 100vh;
overflow-y: auto;
}
</style>
</head>
<body class="bg-gray-50">
<div id="app" v-cloak>
<div class="w-full h-full px-6 py-6">
<!-- Prompt List Card -->
<div class="bg-white rounded-lg shadow p-8 mb-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-gray-800 flex items-center gap-3">
<svg class="w-7 h-7 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
提示词文件列表
</h2>
<button @click="createNewPrompt"
class="px-4 py-2 bg-green-500 hover:bg-green-600 text-white rounded-lg font-bold transition-all flex items-center gap-2 shadow-md hover:shadow-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
新增提示词
</button>
</div>
<!-- Loading -->
<div v-if="isLoadingList" class="text-center py-12">
<svg class="w-12 h-12 mx-auto mb-4 text-purple-600 animate-spin" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<p class="text-gray-600">加载中...</p>
</div>
<!-- Prompt List -->
<div v-else-if="promptList.length > 0" class="space-y-3">
<div v-for="prompt in promptList" :key="prompt.name" @click="selectPrompt(prompt.name)" :class="['bg-gradient-to-r p-4 rounded-lg transition-all border-2 group',
selectedPromptName === prompt.name
? 'from-purple-50 to-purple-100 border-purple-500 shadow-md'
: 'from-gray-50 to-gray-100 border-transparent hover:border-purple-300 hover:shadow-sm']">
<div class="flex justify-between items-center">
<div class="flex items-center gap-3 cursor-pointer flex-1">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<div>
<div class="flex items-center gap-2">
<h4 class="font-semibold text-gray-800">{{ prompt.name }}</h4>
<span v-if="prompt.is_default"
class="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded-full">默认</span>
</div>
<p class="text-sm text-gray-500">{{ formatFileSize(prompt.size) }}</p>
</div>
</div>
<div class="flex items-center gap-2">
<button v-if="!prompt.is_default" @click.stop="setAsDefault(prompt.name)"
class="px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white text-sm rounded transition-colors flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
设为默认
</button>
<button @click.stop="deletePrompt(prompt.name)" :disabled="promptList.length <= 1" :class="['px-3 py-1 text-white text-sm rounded transition-all flex items-center gap-1',
promptList.length <= 1
? 'bg-gray-400 cursor-not-allowed hidden'
: 'bg-red-500 hover:bg-red-600 hidden group-hover:flex']">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
删除
</button>
<svg v-if="selectedPromptName === prompt.name" class="w-6 h-6 text-purple-600" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-12">
<svg class="w-16 h-16 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<p class="text-gray-500">暂无提示词文件</p>
</div>
</div>
<!-- Editor Card -->
<div class="bg-white rounded-lg shadow p-8 mb-6">
<h2 class="text-2xl font-bold mb-6 text-gray-800 flex items-center gap-3">
<svg class="w-7 h-7 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
编辑提示词
</h2>
<!-- File Name -->
<div class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">文件名</label>
<input v-model="fileName" type="text" placeholder="提示词文件名称: XXX.md"
class="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:outline-none focus:border-purple-500 transition-colors">
</div>
<!-- Content -->
<div class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">提示词内容</label>
<textarea v-model="promptContent" placeholder="在这里输入提示词内容..."
class="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:outline-none focus:border-purple-500 transition-colors font-mono min-h-[400px] resize-y leading-relaxed"></textarea>
</div>
<!-- Action Buttons -->
<div class="flex gap-4">
<button @click="savePrompt" :disabled="isSaving"
:class="['flex-1 py-3 rounded-lg font-bold text-lg transition-all flex items-center justify-center gap-3',
isSaving ? 'bg-gray-400 cursor-not-allowed' : 'bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white shadow-lg hover:shadow-xl']">
<svg v-if="!isSaving" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
</svg>
<svg v-else class="w-6 h-6 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{{ isSaving ? '保存中...' : '保存提示词' }}
</button>
<button @click="loadPrompt" :disabled="isLoading"
class="flex-1 py-3 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-lg font-bold text-lg transition-all flex items-center justify-center gap-3 disabled:bg-gray-400 disabled:cursor-not-allowed">
<svg v-if="!isLoading" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
<svg v-else class="w-6 h-6 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{{ isLoading ? '加载中...' : '加载当前提示词' }}
</button>
</div>
<!-- Message -->
<div v-if="message.text"
:class="['mt-4 p-4 rounded-lg flex items-start gap-3 border',
message.type === 'success' ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200']">
<svg v-if="message.type === 'success'" class="w-6 h-6 text-green-500 flex-shrink-0" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<svg v-else class="w-6 h-6 text-red-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p :class="message.type === 'success' ? 'text-green-700' : 'text-red-700'">{{ message.text }}</p>
</div>
</div>
<!-- Usage Info Card -->
<div class="bg-white rounded-lg shadow p-8">
<h2 class="text-2xl font-bold mb-6 text-gray-800 flex items-center gap-3">
<svg class="w-7 h-7 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
使用说明
</h2>
<div class="space-y-6 text-gray-700">
<div>
<h3 class="font-bold text-lg mb-3 flex items-center gap-2">
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
如何使用
</h3>
<ol class="space-y-2 ml-7 list-decimal list-inside">
<li><strong>新增提示词:</strong>点击"新增提示词"按钮,输入文件名和内容后保存</li>
<li><strong>编辑提示词:</strong>点击列表中的提示词文件,编辑内容后保存</li>
<li><strong>删除提示词:</strong>点击提示词右侧的"删除"按钮(至少保留一个文件)</li>
<li><strong>设为默认:</strong>点击"设为默认"按钮,将该提示词设为默认使用</li>
<li>文件名建议使用 .md 或 .txt 后缀</li>
<li>所有提示词保存到 prompts 目录</li>
</ol>
</div>
<div>
<h3 class="font-bold text-lg mb-3 flex items-center gap-2">
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
提示
</h3>
<ul class="space-y-2 ml-7 list-disc list-inside">
<li>系统默认使用 "改写提示词.md" 文件(列表中已隐藏)</li>
<li>可以创建多个提示词文件保存不同的提示词方案</li>
<li>点击"设为默认"会将选中文件的内容复制到 "改写提示词.md"</li>
<li>编辑并保存标记为"默认"的提示词时,会自动同步更新默认文件</li>
<li>删除标记为"默认"的提示词时,系统会自动选择另一个文件作为默认</li>
<li>至少需要保留一个提示词文件,不能删除最后一个文件</li>
<li>支持 Markdown 和文本格式</li>
<li>提示词更新后,MCP 工具会立即使用新的提示词</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive, onMounted } = Vue;
// 如果不在 iframe 中,提示用户访问主页面
if (window.self === window.top) {
const useMainPage = confirm('建议使用主页面以获得更好的体验,是否跳转?');
if (useMainPage) {
window.location.href = 'index.html?tab=prompt';
}
}
createApp({
setup() {
// 响应式数据
const promptList = ref([]);
const isLoadingList = ref(false);
const fileName = ref('');
const promptContent = ref('');
const selectedPromptName = ref('');
const isSaving = ref(false);
const isLoading = ref(false);
const message = reactive({
text: '',
type: 'success'
});
// 方法
const loadPromptList = async (autoSelectDefault = false) => {
isLoadingList.value = true;
try {
const response = await fetch('/api/prompts');
const data = await response.json();
if (data.success) {
promptList.value = data.files;
// 如果需要自动选择默认文件(初始化时)
if (autoSelectDefault) {
// 查找标记为默认的文件
const defaultFile = data.files.find(f => f.is_default);
if (defaultFile) {
await selectPrompt(defaultFile.name);
} else if (data.files.length > 0) {
// 如果没有默认文件,选择第一个
await selectPrompt(data.files[0].name);
}
}
} else {
showMessage('加载提示词列表失败: ' + data.error, 'error');
}
} catch (error) {
showMessage('网络错误: ' + error.message, 'error');
} finally {
isLoadingList.value = false;
}
};
const selectPrompt = async (name) => {
selectedPromptName.value = name;
fileName.value = name;
await loadPrompt();
};
const loadPrompt = async () => {
if (!fileName.value.trim()) {
showMessage('请输入文件名', 'error');
return;
}
isLoading.value = true;
message.text = '';
try {
const response = await fetch(`/api/prompts/get?file_name=${encodeURIComponent(fileName.value)}`);
const data = await response.json();
if (data.success) {
promptContent.value = data.content;
selectedPromptName.value = data.fileName;
showMessage('加载成功!', 'success');
} else {
showMessage(data.error || '加载失败', 'error');
}
} catch (error) {
showMessage('网络错误: ' + error.message, 'error');
} finally {
isLoading.value = false;
}
};
const savePrompt = async () => {
if (!fileName.value.trim()) {
showMessage('请输入文件名', 'error');
return;
}
if (!promptContent.value.trim()) {
showMessage('请输入提示词内容', 'error');
return;
}
// 自动添加 .md 后缀(如果没有的话)
let finalFileName = fileName.value.trim();
if (!finalFileName.toLowerCase().endsWith('.md')) {
finalFileName += '.md';
fileName.value = finalFileName; // 更新输入框显示
}
isSaving.value = true;
message.text = '';
try {
const response = await fetch('/api/prompts/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: finalFileName,
content: promptContent.value
})
});
const data = await response.json();
if (data.success) {
showMessage('保存成功!', 'success');
const savedFileName = fileName.value;
await loadPromptList();
// 保持选中刚保存的文件
selectedPromptName.value = savedFileName;
} else {
showMessage(data.error || '保存失败', 'error');
}
} catch (error) {
showMessage('网络错误: ' + error.message, 'error');
} finally {
isSaving.value = false;
}
};
const showMessage = (text, type = 'success') => {
message.text = text;
message.type = type;
if (type === 'success') {
setTimeout(() => {
message.text = '';
}, 3000);
}
};
const formatFileSize = (bytes) => {
if (bytes < 1024) return bytes + ' bytes';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
};
const setAsDefault = async (name) => {
if (!confirm(`确定要将 "${name}" 的内容设置为默认提示词吗?\n\n此操作会将该文件的内容复制到 "改写提示词.md" 文件中。`)) {
return;
}
try {
const response = await fetch('/api/prompts/set_default', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: name
})
});
const data = await response.json();
if (data.success) {
showMessage(data.message, 'success');
// 重新加载列表以更新默认标记
await loadPromptList();
// 保持选中当前文件并重新加载其内容
await selectPrompt(name);
} else {
showMessage(data.error || '设置失败', 'error');
}
} catch (error) {
showMessage('网络错误: ' + error.message, 'error');
}
};
const createNewPrompt = () => {
// 清空表单,准备创建新提示词
fileName.value = '';
promptContent.value = '';
selectedPromptName.value = '';
message.text = '';
showMessage('请输入新提示词的文件名和内容', 'success');
};
const deletePrompt = async (name) => {
if (promptList.value.length <= 1) {
showMessage('至少需要保留一个提示词文件', 'error');
return;
}
if (!confirm(`确定要删除 "${name}" 吗?\n\n此操作不可恢复。`)) {
return;
}
try {
const response = await fetch('/api/prompts/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: name
})
});
const data = await response.json();
if (data.success) {
showMessage(data.message, 'success');
// 如果删除的是当前选中的文件,清空表单
if (selectedPromptName.value === name) {
fileName.value = '';
promptContent.value = '';
selectedPromptName.value = '';
}
// 重新加载列表
await loadPromptList(true);
} else {
showMessage(data.error || '删除失败', 'error');
}
} catch (error) {
showMessage('网络错误: ' + error.message, 'error');
}
};
// 组件挂载时加载提示词列表并自动选择默认文件
onMounted(() => {
loadPromptList(true);
});
return {
promptList,
isLoadingList,
fileName,
promptContent,
selectedPromptName,
isSaving,
isLoading,
message,
loadPromptList,
selectPrompt,
loadPrompt,
savePrompt,
showMessage,
formatFileSize,
setAsDefault,
createNewPrompt,
deletePrompt
};
}
}).mount('#app');
</script>
</body>
</html>