实现客户端部分的 prompt 支持

This commit is contained in:
锦恢 2025-05-06 21:53:43 +08:00
parent cf16cff7cf
commit 5a2a699a51
15 changed files with 173 additions and 187 deletions

View File

@ -52,7 +52,7 @@
<footer class="chat-footer" ref="footerRef">
<div class="input-area">
<div class="input-wrapper">
<Setting :tabId="tabId" />
<Setting :tabId="tabId" v-model="userInput" />
<KCuteTextarea
v-model="userInput"

View File

@ -1,37 +1,74 @@
<template>
<el-tooltip :content="t('context-length')" placement="top">
<div class="setting-button" @click="showContextLengthDialog = true">
<span class="iconfont icon-length"></span>
<span class="value-badge">{{ tabStorage.settings.contextLength }}</span>
<el-tooltip :content="t('prompts')" placement="top">
<div class="setting-button" @click="showChoosePrompt = true">
<span class="iconfont icon-chat"></span>
</div>
</el-tooltip>
<!-- 上下文长度设置 - 改为滑块形式 -->
<el-dialog v-model="showContextLengthDialog" :title="t('context-length') + ' ' + tabStorage.settings.contextLength"
width="400px">
<div class="slider-container">
<el-slider v-model="tabStorage.settings.contextLength" :min="1" :max="99" :step="1" />
<div class="slider-tips">
<span> 1: {{ t('single-dialog') }}</span>
<span> >1: {{ t('multi-dialog') }}</span>
<el-dialog v-model="showChoosePrompt" :title="t('prompts')" width="400px">
<div class="prompt-template-container-scrollbar" v-if="!selectPrompt">
<PromptTemplates
:tab-id="-1"
@prompt-selected="prompt => selectPrompt = prompt"
/>
</div>
<div v-else>
<PromptReader
:tab-id="-1"
:current-prompt-name="selectPrompt!.name"
@prompt-get-response="msg => whenGetPromptResponse(msg)"
/>
</div>
<template #footer>
<el-button @click="showContextLengthDialog = false">{{ t("cancel") }}</el-button>
<el-button v-if="selectPrompt" @click="selectPrompt = undefined;">{{ t('return') }}</el-button>
<el-button @click="showChoosePrompt = false; selectPrompt = undefined;">{{ t("cancel") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { defineComponent, inject, ref } from 'vue';
import { inject, ref, defineProps, PropType, defineEmits } from 'vue';
import { useI18n } from 'vue-i18n';
import { ChatStorage } from '../chat';
import { PromptsGetResponse, PromptTemplate } from '@/hook/type';
import PromptTemplates from '../../prompt/prompt-templates.vue';
import PromptReader from '../../prompt/prompt-reader.vue';
import { ElMessage } from 'element-plus';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
const showContextLengthDialog = ref(false);
const props = defineProps({
modelValue: {
type: String,
required: true
}
});
const emits = defineEmits([ 'update:modelValue' ]);
let selectPrompt = ref<PromptTemplate | undefined>(undefined);
const showChoosePrompt = ref(false);
function whenGetPromptResponse(msg: PromptsGetResponse) {
try {
const content = msg.messages[0].content;
if (content) {
emits('update:modelValue', props.modelValue + content);
}
showChoosePrompt.value = false;
} catch (error) {
ElMessage.error((error as Error).message);
}
}
</script>

View File

@ -3,6 +3,7 @@
<Model />
<SystemPrompt />
<ToolUse />
<Prompt v-model="val" />
<Websearch />
<Temperature />
<ContextLength />
@ -10,7 +11,7 @@
</template>
<script setup lang="ts">
import { defineProps, provide } from 'vue';
import { defineProps, provide, ref } from 'vue';
import { llmManager } from '@/views/setting/llm';
import { tabs } from '../../panel';
import type { ChatSetting, ChatStorage } from '../chat';
@ -18,17 +19,24 @@ import type { ChatSetting, ChatStorage } from '../chat';
import Model from './model.vue';
import SystemPrompt from './system-prompt.vue';
import ToolUse from './tool-use.vue';
import Prompt from './prompt.vue';
import Websearch from './websearch.vue';
import Temperature from './temperature.vue';
import ContextLength from './context-length.vue';
const props = defineProps({
modelValue: {
type: String,
required: true
},
tabId: {
type: Number,
required: true
}
});
const val = ref('');
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ChatStorage & { settings: ChatSetting };

View File

@ -32,12 +32,12 @@
</template>
<script setup lang="ts">
import { defineComponent, defineProps, watch, ref, computed } from 'vue';
import { defineComponent, defineProps, defineEmits, watch, ref, computed, reactive } 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 { promptsManager, PromptStorage } from './prompts';
import { PromptsGetResponse } from '@/hook/type';
import { useMessageBridge } from '@/api/message-bridge';
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
@ -49,11 +49,26 @@ const props = defineProps({
tabId: {
type: Number,
required: true
},
currentPromptName: {
type: String,
required: false
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as PromptStorage;
const emits = defineEmits(['prompt-get-response']);
let tabStorage: PromptStorage;
if (props.tabId >= 0) {
tabStorage = tabs.content[props.tabId].storage as PromptStorage;
} else {
tabStorage = reactive({
currentPromptName: props.currentPromptName || '',
formData: {},
lastPromptGetResponse: undefined
});
}
if (!tabStorage.formData) {
tabStorage.formData = {};
@ -108,7 +123,6 @@ const initFormData = () => {
newSchemaDataForm[param.name] = tabStorage.formData[param.name];
}
});
}
const resetForm = () => {
@ -116,24 +130,25 @@ const resetForm = () => {
responseData.value = undefined;
}
function handleSubmit() {
async 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: JSON.parse(JSON.stringify(tabStorage.formData)) }
const { code, msg } = await bridge.commandRequest('prompts/get', {
promptId: currentPrompt.value.name,
args: JSON.parse(JSON.stringify(tabStorage.formData))
});
tabStorage.lastPromptGetResponse = msg;
emits('prompt-get-response', msg);
}
watch(() => tabStorage.currentPromptName, () => {
if (props.tabId >= 0) {
watch(() => tabStorage.currentPromptName, () => {
initFormData();
resetForm();
}, { immediate: true });
}, { immediate: true });
}
</script>
<style>

View File

@ -12,7 +12,7 @@
<div class="prompt-template-container">
<div
class="item"
:class="{ 'active': tabStorage.currentPromptName === template.name }"
:class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }"
v-for="template of promptsManager.templates"
:key="template.name"
@click="handleClick(template)"
@ -28,7 +28,7 @@
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, PromptTemplate, PromptsListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps } from 'vue';
import { onMounted, onUnmounted, defineProps, defineEmits, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { promptsManager, PromptStorage } from './prompts';
import { tabs } from '../panel';
@ -44,8 +44,20 @@ const props = defineProps({
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as PromptStorage;
const emits = defineEmits([ 'prompt-selected' ]);
let tabStorage: PromptStorage;
if (props.tabId >= 0) {
const tab = tabs.content[props.tabId];
tabStorage = tab.storage as PromptStorage;
} else {
tabStorage = reactive({
currentPromptName: '',
formData: {},
lastPromptGetResponse: undefined
});
}
function reloadPrompts(option: { first: boolean }) {
bridge.postMessage({
@ -62,9 +74,11 @@ function reloadPrompts(option: { first: boolean }) {
}
}
function handleClick(template: PromptTemplate) {
tabStorage.currentPromptName = template.name;
function handleClick(prompt: PromptTemplate) {
tabStorage.currentPromptName = prompt.name;
tabStorage.lastPromptGetResponse = undefined;
emits('prompt-selected', prompt);
}
let commandCancel: (() => void);

View File

@ -151,5 +151,6 @@
"generate-answer": "جارٍ إنشاء الإجابة",
"choose-presetting": "اختر الإعداد المسبق",
"cwd": "دليل التنفيذ",
"mcp-server-timeout": "أطول وقت لاستدعاء أداة MCP"
"mcp-server-timeout": "أطول وقت لاستدعاء أداة MCP",
"return": "عودة"
}

View File

@ -151,5 +151,6 @@
"generate-answer": "Antwort wird generiert",
"choose-presetting": "Voreinstellung auswählen",
"cwd": "Ausführungsverzeichnis",
"mcp-server-timeout": "Maximale Aufrufzeit des MCP-Tools"
"mcp-server-timeout": "Maximale Aufrufzeit des MCP-Tools",
"return": "Zurück"
}

View File

@ -151,5 +151,6 @@
"generate-answer": "Generating answer",
"choose-presetting": "Select preset",
"cwd": "Execution directory",
"mcp-server-timeout": "Maximum call time of MCP tool"
"mcp-server-timeout": "Maximum call time of MCP tool",
"return": "Back"
}

View File

@ -151,5 +151,6 @@
"generate-answer": "Génération de la réponse",
"choose-presetting": "Sélectionner un préréglage",
"cwd": "Répertoire d'exécution",
"mcp-server-timeout": "Temps d'appel maximum de l'outil MCP"
"mcp-server-timeout": "Temps d'appel maximum de l'outil MCP",
"return": "Retour"
}

View File

@ -151,5 +151,6 @@
"generate-answer": "回答を生成中",
"choose-presetting": "プリセットを選択",
"cwd": "実行ディレクトリ",
"mcp-server-timeout": "MCPツールの最大呼び出し時間"
"mcp-server-timeout": "MCPツールの最大呼び出し時間",
"return": "戻る"
}

View File

@ -151,5 +151,6 @@
"generate-answer": "답변 생성 중",
"choose-presetting": "프리셋 선택",
"cwd": "실행 디렉터리",
"mcp-server-timeout": "MCP 도구 최대 호출 시간"
"mcp-server-timeout": "MCP 도구 최대 호출 시간",
"return": "돌아가기"
}

View File

@ -151,5 +151,6 @@
"generate-answer": "Генерация ответа",
"choose-presetting": "Выбрать预设",
"cwd": "Каталог выполнения",
"mcp-server-timeout": "Максимальное время вызова инструмента MCP"
"mcp-server-timeout": "Максимальное время вызова инструмента MCP",
"return": "Назад"
}

View File

@ -151,5 +151,6 @@
"generate-answer": "正在生成答案",
"choose-presetting": "选择预设",
"cwd": "执行目录",
"mcp-server-timeout": "MCP工具最长调用时间"
"mcp-server-timeout": "MCP工具最长调用时间",
"return": "返回"
}

View File

@ -151,5 +151,6 @@
"generate-answer": "正在生成答案",
"choose-presetting": "選擇預設",
"cwd": "執行目錄",
"mcp-server-timeout": "MCP工具最長調用時間"
"mcp-server-timeout": "MCP工具最長調用時間",
"return": "返回"
}

View File

@ -7,128 +7,9 @@
"type": "blank",
"componentIndex": 3,
"storage": {
"messages": [
{
"role": "user",
"content": "请问杭州的天气",
"extraInfo": {
"created": 1745258229256,
"serverName": "deepseek"
}
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_0_57ad3eab-cd9d-4403-bdf0-31d00b83b98c",
"index": 0,
"type": "function",
"function": {
"name": "get_weather_by_city_code",
"arguments": "{\"city_code\":101210101}"
}
}
],
"extraInfo": {
"created": 1745258234470,
"serverName": "deepseek",
"usage": {
"prompt_tokens": 570,
"completion_tokens": 26,
"total_tokens": 596,
"prompt_tokens_details": {
"cached_tokens": 512
},
"prompt_cache_hit_tokens": 512,
"prompt_cache_miss_tokens": 58
}
}
},
{
"role": "tool",
"tool_call_id": "call_0_57ad3eab-cd9d-4403-bdf0-31d00b83b98c",
"content": "[{\"type\":\"text\",\"text\":\"CityWeather(city_name_en='hangzhou', city_name_cn='杭州', city_code='101210101', temp='21', wd='', ws='', sd='92%', aqi='13', weather='阴')\"}]",
"extraInfo": {
"created": 1745258234522,
"serverName": "deepseek",
"usage": {
"prompt_tokens": 570,
"completion_tokens": 26,
"total_tokens": 596,
"prompt_tokens_details": {
"cached_tokens": 512
},
"prompt_cache_hit_tokens": 512,
"prompt_cache_miss_tokens": 58
}
}
},
{
"role": "assistant",
"content": "杭州的天气信息如下:\n\n- 城市:杭州\n- 温度21°C\n- 天气状况:阴\n- 湿度92%\n- 空气质量指数 (AQI)13优秀\n\n天气较为舒适适合出行",
"extraInfo": {
"created": 1745258240571,
"serverName": "deepseek",
"usage": {
"prompt_tokens": 660,
"completion_tokens": 52,
"total_tokens": 712,
"prompt_tokens_details": {
"cached_tokens": 576
},
"prompt_cache_hit_tokens": 576,
"prompt_cache_miss_tokens": 84
}
}
},
{
"role": "user",
"content": "再试一次",
"extraInfo": {
"created": 1745495349299,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_jpm4v1e92v4nq650wlqkhm56",
"index": 0,
"type": "function",
"function": {
"name": "get_weather_by_city_code",
"arguments": "{\"city_code\":101210101}"
}
}
],
"extraInfo": {
"created": 1745495350097,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "tool",
"tool_call_id": "call_jpm4v1e92v4nq650wlqkhm56",
"content": "[{\"type\":\"text\",\"text\":\"CityWeather(city_name_en='hangzhou', city_name_cn='杭州', city_code='101210101', temp='18.3', wd='', ws='', sd='89%', aqi='57', weather='阴')\"}]",
"extraInfo": {
"created": 1745495350319,
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "assistant",
"content": "杭州的最新天气信息如下:\n\n- 城市:杭州\n- 温度18.3°C\n- 天气状况:阴\n- 湿度89%\n- 空气质量指数 (AQI)57良好\n\n天气较为凉爽适合外出活动",
"extraInfo": {
"created": 1745495352693,
"serverName": "Huoshan DeepSeek"
}
}
],
"messages": [],
"settings": {
"modelIndex": 0,
"modelIndex": 15,
"enableTools": [
{
"name": "add",
@ -158,29 +39,51 @@
],
"enableWebSearch": false,
"temperature": 0.7,
"contextLength": 10,
"contextLength": 20,
"systemPrompt": ""
}
}
},
{
"name": "工具",
"icon": "icon-tool",
"type": "tool",
"componentIndex": 2,
"name": "交互测试",
"icon": "icon-robot",
"type": "blank",
"componentIndex": 3,
"storage": {
"currentToolName": "get_weather_by_city_code",
"formData": {
"city_code": 0
},
"lastToolCallResponse": {
"content": [
"messages": [],
"settings": {
"modelIndex": 15,
"enableTools": [
{
"type": "text",
"text": "CityWeather(city_name_en='hangzhou', city_name_cn='杭州', city_code='101210101', temp='20.3', wd='', ws='', sd='89%', aqi='38', weather='阴')"
"name": "add",
"description": "对两个数字进行实数域的加法",
"enabled": true
},
{
"name": "multiply",
"description": "对两个数字进行实数域的乘法运算",
"enabled": true
},
{
"name": "is_even",
"description": "判断一个整数是否为偶数",
"enabled": true
},
{
"name": "capitalize",
"description": "将字符串首字母大写",
"enabled": true
},
{
"name": "get_weather_by_city_code",
"description": "根据城市天气预报的城市编码 (int),获取指定城市的天气信息",
"enabled": true
}
],
"isError": false
"enableWebSearch": false,
"temperature": 0.7,
"contextLength": 20,
"systemPrompt": ""
}
}
}