实现 system prompt 的设置和保存

This commit is contained in:
锦恢 2025-04-28 17:31:19 +08:00
parent 27e94efa26
commit 85c26a3cbf
18 changed files with 316 additions and 32 deletions

View File

@ -11,6 +11,8 @@
"core-js": "^3.8.3",
"element-plus": "^2.9.7",
"katex": "^0.16.21",
"loadash": "^1.0.0",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"markdown-it-katex": "^2.0.3",
"openai": "^4.93.0",
@ -20,6 +22,7 @@
"vue-router": "^4.0.3"
},
"devDependencies": {
"@types/lodash": "^4.17.16",
"@types/markdown-it": "^14.1.2",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
@ -8620,6 +8623,13 @@
"uc.micro": "^2.0.0"
}
},
"node_modules/loadash": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/loadash/-/loadash-1.0.0.tgz",
"integrity": "sha512-xlX5HBsXB3KG0FJbJJG/3kYWCfsCyCSus3T+uHVu6QL6YxAdggmm3QeyLgn54N2yi5/UE6xxL5ZWJAAiHzHYEg==",
"deprecated": "Package is unsupport. Please use the lodash package instead.",
"license": "ISC"
},
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz",
@ -8690,7 +8700,7 @@
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},

View File

@ -11,6 +11,7 @@
"core-js": "^3.8.3",
"element-plus": "^2.9.7",
"katex": "^0.16.21",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"markdown-it-katex": "^2.0.3",
"openai": "^4.93.0",
@ -20,6 +21,7 @@
"vue-router": "^4.0.3"
},
"devDependencies": {
"@types/lodash": "^4.17.16",
"@types/markdown-it": "^14.1.2",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",

View File

@ -65,7 +65,6 @@ body::-webkit-scrollbar {
}
.el-textarea__inner {
border-radius: .9em !important;
padding: 10px !important;
box-shadow: 0 0 0 1px var(--main-color) !important;
}

View File

@ -7,6 +7,7 @@
:placeholder="placeholder"
:resize="resize"
:class="customClass"
class="k-cute-textarea"
@keydown.enter="handleKeydown"
@compositionstart="handleCompositionStart"
@compositionend="handleCompositionEnd"
@ -68,3 +69,11 @@ const handleCompositionEnd = () => {
isComposing.value = false;
};
</script>
<style>
.k-cute-textarea textarea {
border-radius: .9em;
}
</style>

View File

@ -1,4 +1,5 @@
import { useMessageBridge } from "@/api/message-bridge";
import { pinkLog } from "@/views/setting/util";
import { ref } from "vue";
interface SystemPrompt {
@ -6,14 +7,50 @@ interface SystemPrompt {
content: string;
}
export const systemPrompt = ref<SystemPrompt>({
name: '默认',
export const systemPrompts = ref<SystemPrompt[]>([{
name: 'Default',
content: '你是一个AI助手, 你可以回答任何问题。'
});
}]);
export function saveSystemPrompts() {
export async function saveSystemPrompts() {
const bridge = useMessageBridge();
return new Promise(resolve => {
})
const payload = JSON.parse(JSON.stringify(systemPrompts.value));
const res = await bridge.commandRequest('system-prompts/save', { prompts: payload });
if (res.code === 200) {
pinkLog('system prompt 保存成功');
}
}
export async function setSystemPrompt(name: string, content: string) {
const bridge = useMessageBridge();
const res = await bridge.commandRequest('system-prompts/set', { name, content });
if (res.code === 200) {
pinkLog('system prompt 添加成功');
if (!systemPrompts.value.some(prompt => prompt.name === name)) {
systemPrompts.value.push({ name, content });
}
}
return res;
}
export async function deleteSystemPrompt(name: string) {
const bridge = useMessageBridge();
const res = await bridge.commandRequest('system-prompts/delete', { name });
if (res.code === 200) {
pinkLog('system prompt 删除成功');
systemPrompts.value = systemPrompts.value.filter((prompt) => prompt.name !== name);
}
return res;
}
export async function loadSystemPrompts() {
const bridge = useMessageBridge();
const res = await bridge.commandRequest('system-prompts/load');
if (res.code === 200) {
pinkLog('system prompt 加载成功');
systemPrompts.value = res.msg;
}
return res;
}

View File

@ -6,33 +6,165 @@
</div>
</el-tooltip>
<!-- System Prompt对话框 -->
<el-dialog v-model="showSystemPromptDialog" :title="t('system-prompt')" width="600px">
<el-input v-model="tabStorage.settings.systemPrompt" type="textarea" :rows="8"
:placeholder="t('system-prompt.placeholder')" clearable />
<template #footer>
<div v-if="!showAdd">
<el-select v-model="tabStorage.settings.systemPrompt" placeholder="选择预设"
style="width: 100%; margin-bottom: 20px;">
<el-option v-for="prompt in systemPrompts"
:value="prompt.name" :key="prompt.name"
class="choose-system-prompt"
>
<span>{{ prompt.name }}</span>
<el-dropdown trigger="hover" @command="handleCommand">
<span>
<span class="iconfont icon-more"></span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ type: 'delete', name: prompt.name }" divided>
<span class="iconfont icon-delete">&emsp;{{ t('delete') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-option>
<el-option label="新建预设" value="$new" @click="showAdd = true; newPromptName = ''; newPromptContent = ''">
<span class="iconfont icon-add"></span>
</el-option>
</el-select>
<el-input v-model="currentPromptValue" type="textarea" :rows="8"
:placeholder="t('system-prompt.placeholder')" clearable />
</div>
<div v-else class="tool-use">
<el-input v-model="newPromptName" :placeholder="t('add-system-prompt.name-placeholder')" />
<el-input v-model="newPromptContent" type="textarea" :rows="8" :placeholder="t('system-prompt.placeholder')"
clearable />
</div>
<template #footer v-if="!showAdd">
<el-button @click="showSystemPromptDialog = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="showSystemPromptDialog = false">{{ t("save") }}</el-button>
</template>
<template #footer v-else>
<el-button @click="showAdd = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="addNewPrompt">{{ t("save") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, inject } from 'vue';
import { ref, computed, inject, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { ChatStorage } from '../chat';
import { systemPrompts, saveSystemPrompts, setSystemPrompt, loadSystemPrompts, deleteSystemPrompt } from './system-prompt';
import { ElMessage } from 'element-plus';
import { debounce } from 'lodash';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
const showSystemPromptDialog = ref(false);
const showAdd = ref(false);
const hasSystemPrompt = computed(() => {
return !!tabStorage.settings.systemPrompt?.trim();
return !!tabStorage.settings.systemPrompt?.trim();
});
const newPromptName = ref('');
const newPromptContent = ref('');
const currentPromptValue = computed({
get() {
return systemPrompts.value.find(prompt => prompt.name === tabStorage.settings.systemPrompt)?.content || '';
},
set(value) {
const prompt = systemPrompts.value.find(prompt => prompt.name === tabStorage.settings.systemPrompt);
if (prompt) {
prompt.content = value;
safeSaveSystemPrompts();
}
}
});
async function addNewPrompt() {
const name = newPromptName.value.trim();
const content = newPromptContent.value.trim();
//
if (systemPrompts.value.some(prompt => prompt.name === name)) {
ElMessage.warning('预设名称已存在,请选择其他名称。');
return;
}
const res = await setSystemPrompt(name, content);
if (res.code === 200) {
ElMessage.success('预设添加成功。');
showAdd.value = false;
tabStorage.settings.systemPrompt = name;
} else {
ElMessage.error('添加预设失败。' + res.msg);
}
}
const safeSaveSystemPrompts = debounce(async () => {
if (!showAdd.value) {
const prompt = systemPrompts.value.find(prompt => prompt.name === tabStorage.settings.systemPrompt);
if (prompt) {
await setSystemPrompt(prompt.name, prompt.content);
}
}
}, 500);
async function handleCommand(command: {type: string, name: string}) {
if (command.type === 'delete') {
const res = await deleteSystemPrompt(command.name);
if (res.code === 200) {
if (tabStorage.settings.systemPrompt === command.name) {
tabStorage.settings.systemPrompt = systemPrompts.value[0]?.name || '';
}
}
}
}
onMounted(async () => {
await loadSystemPrompts();
});
</script>
<style></style>
<style>
.setting-button {
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background-color 0.3s;
}
.setting-button:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.setting-button.active {
color: var(--el-color-primary);
}
.tool-use .el-input__wrapper {
margin-bottom: 20px;
border-radius: .5em;
}
.choose-system-prompt {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
</style>

View File

@ -35,10 +35,11 @@
</template>
<script setup lang="ts">
import { ref, computed, inject } from 'vue';
import { ref, computed, inject, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { ChatStorage, getToolSchema } from '../chat';
import { allTools, ChatStorage, getToolSchema } from '../chat';
import { markdownToHtml } from '../markdown/markdown';
import { useMessageBridge } from '@/api/message-bridge';
const { t } = useI18n();
@ -80,6 +81,22 @@ const disableAllTools = () => {
});
};
onMounted(async () => {
const bridge = useMessageBridge();
const res = await bridge.commandRequest('tools/list');
if (res.code === 200) {
allTools.value = res.msg.tools || [];
tabStorage.settings.enableTools = [];
for (const tool of allTools.value) {
tabStorage.settings.enableTools.push({
name: tool.name,
description: tool.description,
enabled: true
});
}
}
});
</script>
<style></style>

View File

@ -145,5 +145,6 @@
"multi-dialog": "محادثة متعددة الجولات",
"press-and-run": "اكتب سؤالاً لبدء الاختبار",
"connect-sigature": "توقيع الاتصال",
"finish-refresh": "تم التحديث"
"finish-refresh": "تم التحديث",
"add-system-prompt.name-placeholder": "عنوان prompt المخصص"
}

View File

@ -145,5 +145,6 @@
"multi-dialog": "Mehrrundengespräch",
"press-and-run": "Geben Sie eine Frage ein, um den Test zu starten",
"connect-sigature": "Verbindungssignatur",
"finish-refresh": "Aktualisierung abgeschlossen"
"finish-refresh": "Aktualisierung abgeschlossen",
"add-system-prompt.name-placeholder": "Titel für benutzerdefinierte Eingabeaufforderung"
}

View File

@ -145,5 +145,6 @@
"multi-dialog": "Multi-turn conversation",
"press-and-run": "Type a question to start the test",
"connect-sigature": "Connection signature",
"finish-refresh": "Refresh completed"
"finish-refresh": "Refresh completed",
"add-system-prompt.name-placeholder": "Title for custom prompt"
}

View File

@ -145,5 +145,6 @@
"multi-dialog": "Conversation multi-tours",
"press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "Signature de connexion",
"finish-refresh": "Actualisation terminée"
"finish-refresh": "Actualisation terminée",
"add-system-prompt.name-placeholder": "Titre de l'invite personnalisée"
}

View File

@ -145,5 +145,6 @@
"multi-dialog": "マルチターン会話",
"press-and-run": "テストを開始するには質問を入力してください",
"connect-sigature": "接続署名",
"finish-refresh": "更新が完了しました"
"finish-refresh": "更新が完了しました",
"add-system-prompt.name-placeholder": "カスタムプロンプトのタイトル"
}

View File

@ -145,5 +145,6 @@
"multi-dialog": "다중 턴 대화",
"press-and-run": "테스트를 시작하려면 질문을 입력하세요",
"connect-sigature": "연결 서명",
"finish-refresh": "새로 고침 완료"
"finish-refresh": "새로 고침 완료",
"add-system-prompt.name-placeholder": "사용자 지정 프롬프트 제목"
}

View File

@ -145,5 +145,6 @@
"multi-dialog": "Многораундовый разговор",
"press-and-run": "Введите вопрос, чтобы начать тест",
"connect-sigature": "Подпись соединения",
"finish-refresh": "Обновление завершено"
"finish-refresh": "Обновление завершено",
"add-system-prompt.name-placeholder": "Заголовок пользовательского prompt"
}

View File

@ -145,5 +145,6 @@
"multi-dialog": "多轮对话",
"press-and-run": "键入问题以开始测试",
"connect-sigature": "连接签名",
"finish-refresh": "完成刷新"
"finish-refresh": "完成刷新",
"add-system-prompt.name-placeholder": "输入自定义 prompt 的标题"
}

View File

@ -145,5 +145,6 @@
"multi-dialog": "多輪對話",
"press-and-run": "輸入問題以開始測試",
"connect-sigature": "連接簽名",
"finish-refresh": "刷新完成"
"finish-refresh": "刷新完成",
"add-system-prompt.name-placeholder": "自定義提示的標題"
}

View File

@ -146,7 +146,13 @@ interface OcrItem extends Entity {
createTime: number;
}
interface SystemPromptItem extends Entity {
name: string;
content: string;
}
export const diskStorage = new DiskStorage();
export const settingDB = new LocalDB<SettingItem>('setting');
export const ocrDB = new LocalDB<OcrItem>('ocr');
export const systemPromptDB = new LocalDB<SystemPromptItem>('systemPrompt');

View File

@ -1,5 +1,6 @@
import { Controller, RequestClientType } from "../common";
import { PostMessageble } from "../hook/adapter";
import { systemPromptDB } from "../hook/db";
import { loadTabSaveConfig, saveTabSaveConfig } from "./panel.service";
export class PanelController {
@ -25,4 +26,66 @@ export class PanelController {
msg: config
};
}
@Controller('system-prompts/set')
async setSystemPrompt(client: RequestClientType, data: any, webview: PostMessageble) {
const { name, content } = data;
await systemPromptDB.insert({
id: name,
name,
content
});
return {
code: 200,
msg: 'Settings saved successfully'
}
}
@Controller('system-prompts/delete')
async deleteSystemPrompt(client: RequestClientType, data: any, webview: PostMessageble) {
const { name } = data;
await systemPromptDB.delete(name);
return {
code: 200,
msg: 'Settings saved successfully'
}
}
@Controller('system-prompts/save')
async saveSystemPrompts(client: RequestClientType, data: any, webview: PostMessageble) {
const { prompts } = data;
await Promise.all(prompts.map((prompt: any) => {
systemPromptDB.insert({
id: prompt.name,
name: prompt.name,
content: prompt.content
})
}));
return {
code: 200,
msg: 'Settings saved successfully'
}
}
@Controller('system-prompts/load')
async loadSystemPrompts(client: RequestClientType, data: any, webview: PostMessageble) {
const queryPrompts = await systemPromptDB.findAll();
const prompts = [];
for (const prompt of queryPrompts) {
prompts.push({
name: prompt.name,
content: prompt.content
})
}
return {
code: 200,
msg: prompts
}
}
}