完成 prompts,但是还有点 bug

This commit is contained in:
锦恢 2025-03-31 23:39:29 +08:00
parent 2f104cda6d
commit 5564dec451
19 changed files with 639 additions and 64 deletions

View File

@ -1,14 +1,34 @@
<template> <template>
<div class="lexicon-module"> <div class="prompt-module">
<h2>题词模块</h2> <div class="left">
<!-- 题词模块内容将在这里实现 --> <h2>
</div> <span class="iconfont icon-chat"></span>
提示词模块
</h2>
<h3><code>prompts/list</code></h3>
<PromptTemplates
:tab-id="props.tabId"
></PromptTemplates>
</div>
<div class="right">
<PromptReader
:tab-id="props.tabId"
></PromptReader>
<PromptLogger
:tab-id="props.tabId"
></PromptLogger>
</div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, defineProps } from 'vue'; import { defineProps } from 'vue';
import PromptTemplates from './prompt-templates.vue';
defineComponent({ name: 'prompt' }); import PromptReader from './prompt-reader.vue';
import PromptLogger from './prompt-logger.vue';
const props = defineProps({ const props = defineProps({
tabId: { tabId: {
@ -20,8 +40,19 @@ const props = defineProps({
</script> </script>
<style scoped> <style scoped>
.lexicon-module { .prompt-module {
padding: 20px; padding: 20px;
height: 100%; height: 100%;
display: flex;
justify-content: space-around;
}
.prompt-module .left {
width: 45%;
max-width: 410px;
}
.prompt-module .right {
width: 45%;
} }
</style> </style>

View File

@ -0,0 +1,104 @@
<template>
<div class="prompt-logger">
<span>
<span>{{ t('response') }}</span>
<span style="width: 200px;">
<el-switch
v-model="showRawJson"
inline-prompt
active-text="JSON"
inactive-text="Text"
style="margin-left: 10px; width: 200px;"
:inactive-action-style="'backgroundColor: var(--sidebar)'"
/>
</span>
</span>
<el-scrollbar height="350px">
<div
class="output-content"
contenteditable="false"
>
<template v-if="!showRawJson">
<span v-for="(message, index) of tabStorage.lastPromptGetResponse?.messages || []" :key="index">
{{ message.content.text }}
</span>
</template>
<template v-else>
{{ formattedJson }}
</template>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { defineComponent, defineProps, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { tabs } from '../panel';
import { PromptStorage } from './prompts';
defineComponent({ name: 'prompt-logger' });
const { t } = useI18n();
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as PromptStorage;
const showRawJson = ref(false);
const formattedJson = computed(() => {
try {
return JSON.stringify(tabStorage.lastPromptGetResponse, null, 2);
} catch {
return 'Invalid JSON';
}
});
</script>
<style>
.prompt-logger {
border-radius: .5em;
background-color: var(--background);
padding: 10px;
}
.prompt-logger .el-switch__core {
border: 1px solid var(--main-color) !important;
width: 60px !important;
}
.prompt-logger .el-switch .el-switch__action {
background-color: var(--main-color);
}
.prompt-logger .el-switch.is-checked .el-switch__action {
background-color: var(--sidebar);
}
.prompt-logger > span:first-child {
margin-bottom: 5px;
display: flex;
align-items: center;
}
.prompt-logger .output-content {
border-radius: .5em;
padding: 15px;
min-height: 300px;
height: fit-content;
font-family: var(--code-font-family);
white-space: pre-wrap;
word-break: break-all;
user-select: text;
cursor: text;
font-size: 15px;
line-height: 1.5;
background-color: var(--sidebar);
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<div>
<h3>{{ currentPrompt.name }}</h3>
</div>
<div class="prompt-reader-container">
<el-form :model="formData" :rules="formRules" ref="formRef" label-position="top">
<el-form-item v-for="param in currentPrompt?.params" :key="param.name"
:label="param.name" :prop="param.name">
<el-input v-if="param.type === 'string'" v-model="formData[param.name]"
:placeholder="param.placeholder || `请输入${param.name}`" />
<el-input-number v-else-if="param.type === 'number'" v-model="formData[param.name]"
:placeholder="param.placeholder || `请输入${param.name}`" />
<el-switch v-else-if="param.type === 'boolean'" v-model="formData[param.name]" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleSubmit">
{{ t('read-prompt') }}
</el-button>
<el-button @click="resetForm">
{{ t('reset') }}
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { defineComponent, defineProps, watch, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel';
import { parsePromptTemplate, promptsManager, PromptStorage } from './prompts';
import { CasualRestAPI, PromptsGetResponse } from '@/hook/type';
import { useMessageBridge } from '@/api/message-bridge';
defineComponent({ name: 'prompt-reader' });
const { t } = useI18n();
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as PromptStorage;
const formRef = ref<FormInstance>();
const formData = ref<Record<string, any>>({});
const loading = ref(false);
const responseData = ref<PromptsGetResponse>();
const currentPrompt = computed(() => {
const template = promptsManager.templates.find(template => template.name === tabStorage.currentPromptName);
const name = template?.name || '';
const params = template?.arguments || [];
const viewParams = params.map(param => ({
name: param.name,
type: 'string',
placeholder: t('enter') +' ' + param.name,
required: param.required
}));
return {
name,
params: viewParams
};
});
const formRules = computed<FormRules>(() => {
const rules: FormRules = {}
currentPrompt.value?.params.forEach(param => {
rules[param.name] = [
{
message: `${param.name} 是必填字段`,
trigger: 'blur'
}
]
});
return rules;
});
const initFormData = () => {
formData.value = {}
currentPrompt.value?.params.forEach(param => {
formData.value[param.name] = param.type === 'number' ? 0 :
param.type === 'boolean' ? false : ''
})
}
const resetForm = () => {
formRef.value?.resetFields();
responseData.value = undefined;
}
function handleSubmit() {
const bridge = useMessageBridge();
bridge.addCommandListener('prompts/get', (data: CasualRestAPI<PromptsGetResponse>) => {
tabStorage.lastPromptGetResponse = data.msg;
}, { once: true });
bridge.postMessage({
command: 'prompts/get',
data: { promptId: currentPrompt.value.name, args: formData.value }
});
}
watch(() => tabStorage.currentPromptName, () => {
initFormData();
resetForm();
}, { immediate: true });
</script>
<style>
.prompt-reader-container {
background-color: var(--background);
padding: 10px 12px;
border-radius: .5em;
margin-bottom: 15px;
}
</style>

View File

@ -0,0 +1,155 @@
<template>
<div class="prompt-template-container-scrollbar">
<el-scrollbar height="500px">
<div class="prompt-template-container">
<div
class="item"
:class="{ 'active': tabStorage.currentPromptName === template.name }"
v-for="template of promptsManager.templates"
:key="template.name"
@click="handleClick(template)"
>
<span>{{ template.name }}</span>
<span>{{ template.description || '' }}</span>
</div>
</div>
</el-scrollbar>
</div>
<div class="prompt-template-function-container">
<el-button
type="primary"
@click="reloadPrompts({ first: false })"
>
{{ t('refresh') }}
</el-button>
</div>
</template>
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, PromptTemplate, PromptsListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps } from 'vue';
import { useI18n } from 'vue-i18n';
import { promptsManager, PromptStorage } from './prompts';
import { tabs } from '../panel';
import { ElMessage } from 'element-plus';
const bridge = useMessageBridge();
const { t } = useI18n();
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as PromptStorage;
function reloadPrompts(option: { first: boolean }) {
bridge.postMessage({
command: 'prompts/list'
});
if (!option.first) {
ElMessage({
message: t('finish-refresh'),
type: 'success',
duration: 3000,
showClose: true,
});
}
}
function handleClick(template: PromptTemplate) {
tabStorage.currentPromptName = template.name;
tabStorage.lastPromptGetResponse = undefined;
}
let commandCancel: (() => void);
onMounted(() => {
commandCancel = bridge.addCommandListener('prompts/list', (data: CasualRestAPI<PromptsListResponse>) => {
promptsManager.templates = data.msg.prompts || [];
if (promptsManager.templates.length > 0) {
tabStorage.currentPromptName = promptsManager.templates[0].name;
tabStorage.lastPromptGetResponse = undefined;
}
}, { once: false });
reloadPrompts({ first: true });
});
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script>
<style>
.prompt-template-container-scrollbar {
background-color: var(--background);
margin-bottom: 10px;
border-radius: .5em;
}
.prompt-template-container {
height: fit-content;
display: flex;
flex-direction: column;
padding: 10px;
}
.prompt-template-function-container {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.prompt-template-function-container button {
width: 175px;
}
.prompt-template-container > .item {
margin: 3px;
padding: 5px 10px;
border-radius: .3em;
user-select: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: var(--animation-3s);
}
.prompt-template-container > .item:hover {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.prompt-template-container > .item.active {
background-color: var(--main-light-color);
transition: var(--animation-3s);
}
.prompt-template-container > .item > span:first-child {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.prompt-template-container > .item > span:last-child {
opacity: 0.6;
font-size: 12.5px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -0,0 +1,51 @@
import { PromptsGetResponse, PromptTemplate } from '@/hook/type';
import { reactive } from 'vue';
export const promptsManager = reactive<{
current: PromptTemplate | undefined
templates: PromptTemplate[]
}>({
current: undefined,
templates: []
});
export interface PromptStorage {
currentPromptName: string;
lastPromptGetResponse?: PromptsGetResponse;
}
export function parsePromptTemplate(template: string): {
params: string[],
fill: (params: Record<string, string>) => string
} {
const paramRegex = /\{([^}]+)\}/g;
const params = new Set<string>();
let match;
while ((match = paramRegex.exec(template)) !== null) {
params.add(match[1]);
}
const paramList = Array.from(params);
const fill = (values: Record<string, string>): string => {
let result = template;
for (const param of paramList) {
if (values[param] === undefined) {
throw new Error(`缺少必要参数: ${param}`);
}
}
for (const param of paramList) {
result = result.replace(new RegExp(`\\{${param}\\}`, 'g'), values[param]);
}
return result;
};
return {
params: paramList,
fill
};
}

View File

@ -46,11 +46,12 @@ const props = defineProps({
padding: 20px; padding: 20px;
height: 100%; height: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-around;
} }
.resource-module .left { .resource-module .left {
width: 45%; width: 45%;
max-width: 410px;
} }
.resource-module .right { .resource-module .right {

View File

@ -1,25 +1,44 @@
<template> <template>
<div class="resource-logger"> <div class="resource-logger">
<span>{{ "Response" }}</span> <span>
<el-scrollbar height="300px"> <span>{{ t('response') }}</span>
<div <span style="width: 200px;">
class="output-content" <el-switch
contenteditable="false" v-model="showRawJson"
> inline-prompt
<span v-for="(content, index) of tabStorage.lastResourceReadResponse?.contents || []" :key="index"> active-text="JSON"
{{ content.text }} inactive-text="Text"
</span> style="margin-left: 10px; width: 200px;"
</div> :inactive-action-style="'backgroundColor: var(--sidebar)'"
</el-scrollbar> />
</span>
</span>
<el-scrollbar height="350px">
<div
class="output-content"
contenteditable="false"
>
<template v-if="!showRawJson">
<span v-for="(content, index) of tabStorage.lastResourceReadResponse?.contents || []" :key="index">
{{ content.text }}
</span>
</template>
<template v-else>
{{ formattedJson }}
</template>
</div>
</el-scrollbar>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, defineProps } from 'vue'; import { defineComponent, defineProps, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ResourceStorage } from './resources'; import { ResourceStorage } from './resources';
defineComponent({ name: 'resource-logger' }); defineComponent({ name: 'resource-logger' });
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
tabId: { tabId: {
@ -31,31 +50,55 @@ const props = defineProps({
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ResourceStorage; const tabStorage = tab.storage as ResourceStorage;
const showRawJson = ref(false);
const formattedJson = computed(() => {
try {
return JSON.stringify(tabStorage.lastResourceReadResponse, null, 2);
} catch {
return 'Invalid JSON';
}
});
</script> </script>
<style> <style>
.resource-logger { .resource-logger {
border-radius: .5em; border-radius: .5em;
background-color: var(--background); background-color: var(--background);
padding: 10px; padding: 10px;
} }
.resource-logger .output-content { .resource-logger .el-switch__core {
border-radius: .5em; border: 1px solid var(--main-color) !important;
padding: 15px; width: 60px !important;
min-height: 300px;
height: fit-content;
font-family: var(--code-font-family);
white-space: pre-wrap;
word-break: break-all;
user-select: text;
cursor: text;
font-size: 15px;
line-height: 1.5;
background-color: var(--sidebar);
} }
.resource-logger .el-switch .el-switch__action {
background-color: var(--main-color);
}
.resource-logger .el-switch.is-checked .el-switch__action {
background-color: var(--sidebar);
}
.resource-logger > span:first-child {
margin-bottom: 5px;
display: flex;
align-items: center;
}
.resource-logger .output-content {
border-radius: .5em;
padding: 15px;
min-height: 300px;
height: fit-content;
font-family: var(--code-font-family);
white-space: pre-wrap;
word-break: break-all;
user-select: text;
cursor: text;
font-size: 15px;
line-height: 1.5;
background-color: var(--sidebar);
}
</style> </style>

View File

@ -15,16 +15,27 @@
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
<div class="resource-template-function-container">
<el-button
type="primary"
@click="reloadResources({ first: false })"
>
{{ t('refresh') }}
</el-button>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type'; import { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps } from 'vue'; import { onMounted, onUnmounted, defineProps } from 'vue';
import { useI18n } from 'vue-i18n';
import { resourcesManager, ResourceStorage } from './resources'; import { resourcesManager, ResourceStorage } from './resources';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ElMessage } from 'element-plus';
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
tabId: { tabId: {
@ -36,10 +47,19 @@ const props = defineProps({
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ResourceStorage; const tabStorage = tab.storage as ResourceStorage;
function reloadResources() { function reloadResources(option: { first: boolean }) {
bridge.postMessage({ bridge.postMessage({
command: 'resources/templates/list' command: 'resources/templates/list'
}); });
if (!option.first) {
ElMessage({
message: t('finish-refresh'),
type: 'success',
duration: 3000,
showClose: true,
});
}
} }
function handleClick(template: ResourceTemplate) { function handleClick(template: ResourceTemplate) {
@ -48,8 +68,10 @@ function handleClick(template: ResourceTemplate) {
tabStorage.lastResourceReadResponse = undefined; tabStorage.lastResourceReadResponse = undefined;
} }
onMounted(() => { let commandCancel: (() => void);
bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
onMounted(() => {
commandCancel = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
resourcesManager.templates = data.msg.resourceTemplates || []; resourcesManager.templates = data.msg.resourceTemplates || [];
if (resourcesManager.templates.length > 0) { if (resourcesManager.templates.length > 0) {
@ -57,17 +79,23 @@ onMounted(() => {
// TODO: // TODO:
tabStorage.lastResourceReadResponse = undefined; tabStorage.lastResourceReadResponse = undefined;
} }
}, { once: true }); }, { once: false });
reloadResources(); reloadResources({ first: true });
}); });
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script> </script>
<style> <style>
.resource-template-container-scrollbar { .resource-template-container-scrollbar {
background-color: var(--background); background-color: var(--background);
margin-bottom: 10px;
border-radius: .5em; border-radius: .5em;
} }
@ -78,6 +106,17 @@ onMounted(() => {
padding: 10px; padding: 10px;
} }
.resource-template-function-container {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.resource-template-function-container button {
width: 175px;
}
.resource-template-container > .item { .resource-template-container > .item {
margin: 3px; margin: 3px;
padding: 5px 10px; padding: 5px 10px;

View File

@ -69,6 +69,7 @@ function toggleConnectionPanel() {
} }
.connected-status-container { .connected-status-container {
user-select: none;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -41,12 +41,14 @@ export interface ToolsListResponse {
}>; }>;
} }
export interface PromptTemplate {
name: string;
description: string;
arguments: Argument[];
}
export interface PromptsListResponse { export interface PromptsListResponse {
prompts: Array<{ prompts: PromptTemplate[];
name: string;
description: string;
arguments: Argument[];
}>;
} }
export interface ResourceTemplate { export interface ResourceTemplate {

View File

@ -109,6 +109,8 @@
"reset": "إعادة تعيين", "reset": "إعادة تعيين",
"read-resource": "قراءة الموارد", "read-resource": "قراءة الموارد",
"enter": "إدخال", "enter": "إدخال",
"connect.appearance.connect": "اتصال", "refresh": "تحديث",
"connect.appearance.reconnect": "إعادة الاتصال" "finish-refresh": "تم التحديث",
"response": "الاستجابة",
"read-prompt": "استخراج الكلمات الرئيسية"
} }

View File

@ -109,6 +109,8 @@
"reset": "Zurücksetzen", "reset": "Zurücksetzen",
"read-resource": "Ressourcen lesen", "read-resource": "Ressourcen lesen",
"enter": "Eingabe", "enter": "Eingabe",
"connect.appearance.connect": "Verbinden", "refresh": "Aktualisieren",
"connect.appearance.reconnect": "Wiederverbinden" "finish-refresh": "Aktualisierung abgeschlossen",
"response": "Antwort",
"read-prompt": "Stichwörter extrahieren"
} }

View File

@ -109,6 +109,8 @@
"reset": "Reset", "reset": "Reset",
"read-resource": "Read resources", "read-resource": "Read resources",
"enter": "Input", "enter": "Input",
"connect.appearance.connect": "Connect", "refresh": "Refresh",
"connect.appearance.reconnect": "Reconnect" "finish-refresh": "Refresh completed",
"response": "Response",
"read-prompt": "Extract keywords"
} }

View File

@ -109,6 +109,8 @@
"reset": "Réinitialiser", "reset": "Réinitialiser",
"read-resource": "Lire les ressources", "read-resource": "Lire les ressources",
"enter": "Entrée", "enter": "Entrée",
"connect.appearance.connect": "Se connecter", "refresh": "Rafraîchir",
"connect.appearance.reconnect": "Reconnecter" "finish-refresh": "Actualisation terminée",
"response": "Réponse",
"read-prompt": "Extraire des mots-clés"
} }

View File

@ -109,6 +109,8 @@
"reset": "リセット", "reset": "リセット",
"read-resource": "リソースを読み込む", "read-resource": "リソースを読み込む",
"enter": "入力", "enter": "入力",
"connect.appearance.connect": "接続", "refresh": "更新",
"connect.appearance.reconnect": "再接続" "finish-refresh": "更新が完了しました",
"response": "応答",
"read-prompt": "キーワードを抽出"
} }

View File

@ -109,6 +109,8 @@
"reset": "재설정", "reset": "재설정",
"read-resource": "리소스 읽기", "read-resource": "리소스 읽기",
"enter": "입력", "enter": "입력",
"connect.appearance.connect": "연결", "refresh": "새로 고침",
"connect.appearance.reconnect": "재연결" "finish-refresh": "새로 고침 완료",
"response": "응답",
"read-prompt": "키워드 추출"
} }

View File

@ -109,6 +109,8 @@
"reset": "Сброс", "reset": "Сброс",
"read-resource": "Чтение ресурсов", "read-resource": "Чтение ресурсов",
"enter": "Ввод", "enter": "Ввод",
"connect.appearance.connect": "Подключиться", "refresh": "Обновить",
"connect.appearance.reconnect": "Переподключиться" "finish-refresh": "Обновление завершено",
"response": "Ответ",
"read-prompt": "Извлечь ключевые слова"
} }

View File

@ -109,6 +109,8 @@
"reset": "重置", "reset": "重置",
"read-resource": "读取资源", "read-resource": "读取资源",
"enter": "输入", "enter": "输入",
"connect.appearance.connect":"连接", "refresh": "刷新",
"connect.appearance.reconnect":"重新连接" "finish-refresh": "刷新完成",
"response": "响应",
"read-prompt": "提取提词"
} }

View File

@ -109,6 +109,8 @@
"reset": "重置", "reset": "重置",
"read-resource": "讀取資源", "read-resource": "讀取資源",
"enter": "輸入", "enter": "輸入",
"connect.appearance.connect": "連接", "refresh": "重新整理",
"connect.appearance.reconnect": "重新連接" "finish-refresh": "刷新完成",
"response": "響應",
"read-prompt": "提取關鍵詞"
} }