重构 chat 模块的 setting 面板
This commit is contained in:
parent
a535690bc6
commit
27e94efa26
@ -7,6 +7,11 @@ export interface VSCodeMessage {
|
||||
callbackId?: string;
|
||||
}
|
||||
|
||||
export interface RestFulResponse {
|
||||
code: number;
|
||||
msg: any;
|
||||
}
|
||||
|
||||
export type MessageHandler = (message: VSCodeMessage) => void;
|
||||
export type CommandHandler = (data: any) => void;
|
||||
|
||||
@ -135,6 +140,25 @@ class MessageBridge {
|
||||
return () => commandHandlers.delete(wrapperCommandHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description do as axios does
|
||||
* @param command
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
public commandRequest(command: string, data?: any) {
|
||||
return new Promise<RestFulResponse>((resolve, reject) => {
|
||||
this.addCommandListener(command, (data) => {
|
||||
resolve(data as RestFulResponse);
|
||||
}, { once: true });
|
||||
|
||||
this.postMessage({
|
||||
command,
|
||||
data
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.ws?.close();
|
||||
this.handlers.clear();
|
||||
@ -151,6 +175,7 @@ export function useMessageBridge() {
|
||||
return {
|
||||
postMessage: bridge.postMessage.bind(bridge),
|
||||
addCommandListener: bridge.addCommandListener.bind(bridge),
|
||||
commandRequest: bridge.commandRequest.bind(bridge),
|
||||
isConnected: bridge.isConnected
|
||||
};
|
||||
}
|
@ -82,7 +82,7 @@ import { TaskLoop } from './task-loop';
|
||||
import { llmManager, llms } from '@/views/setting/llm';
|
||||
|
||||
import * as Message from './message';
|
||||
import Setting from './setting.vue';
|
||||
import Setting from './options/setting.vue';
|
||||
import KCuteTextarea from '@/components/k-cute-textarea/index.vue';
|
||||
|
||||
import { provide } from 'vue';
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
import { markdownToHtml } from '../markdown';
|
||||
import { markdownToHtml } from '../markdown/markdown';
|
||||
|
||||
import MessageMeta from './message-meta.vue';
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
import { markdownToHtml } from '../markdown';
|
||||
import { markdownToHtml } from '../markdown/markdown';
|
||||
|
||||
const props = defineProps({
|
||||
streamingContent: {
|
||||
|
@ -96,7 +96,7 @@
|
||||
import { defineProps, ref, watch, PropType, computed, defineEmits } from 'vue';
|
||||
|
||||
import MessageMeta from './message-meta.vue';
|
||||
import { markdownToHtml } from '../markdown';
|
||||
import { markdownToHtml } from '../markdown/markdown';
|
||||
import { createTest } from '@/views/setting/llm';
|
||||
import { IRenderMessage, MessageState } from '../chat';
|
||||
import { ToolCallContent } from '@/hook/type';
|
||||
|
@ -0,0 +1,42 @@
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showContextLengthDialog = false">{{ t("cancel") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ChatStorage } from '../chat';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const tabStorage = inject('tabStorage') as ChatStorage;
|
||||
|
||||
const showContextLengthDialog = ref(false);
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.icon-length {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
67
renderer/src/components/main-panel/chat/options/model.vue
Normal file
67
renderer/src/components/main-panel/chat/options/model.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<el-tooltip :content="t('choose-model')" placement="top">
|
||||
<div class="setting-button" @click="showModelDialog = true">
|
||||
<span class="iconfont icon-model">
|
||||
{{ currentServerName }}/{{ currentModelName }}
|
||||
</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 模型选择对话框 -->
|
||||
<el-dialog v-model="showModelDialog" :title="t('choose-model')" width="400px">
|
||||
<el-radio-group v-model="selectedModelIndex" @change="onRadioGroupChange">
|
||||
<el-radio v-for="(model, index) in availableModels" :key="index" :value="index">
|
||||
{{ model }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<template #footer>
|
||||
<el-button @click="showModelDialog = false">{{ t("cancel") }}</el-button>
|
||||
<el-button type="primary" @click="confirmModelChange">{{ t("confirm") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { saveSetting } from '@/hook/setting';
|
||||
import { llmManager, llms } from '@/views/setting/llm';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t } = useI18n();
|
||||
|
||||
const showModelDialog = ref(false);
|
||||
const currentModel = llms[llmManager.currentModelIndex].userModel;
|
||||
const selectedModelIndex = ref(llms[llmManager.currentModelIndex].models.indexOf(currentModel));
|
||||
|
||||
const currentServerName = computed(() => {
|
||||
const currentLlm = llms[llmManager.currentModelIndex];
|
||||
if (currentLlm) {
|
||||
return currentLlm.name;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const currentModelName = computed(() => {
|
||||
const currentLlm = llms[llmManager.currentModelIndex];
|
||||
if (currentLlm) {
|
||||
return currentLlm.models[selectedModelIndex.value];
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const availableModels = computed(() => {
|
||||
return llms[llmManager.currentModelIndex].models;
|
||||
});
|
||||
|
||||
const confirmModelChange = () => {
|
||||
showModelDialog.value = false;
|
||||
};
|
||||
|
||||
const onRadioGroupChange = () => {
|
||||
const currentModel = llms[llmManager.currentModelIndex].models[selectedModelIndex.value];
|
||||
llms[llmManager.currentModelIndex].userModel = currentModel;
|
||||
saveSetting();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>
|
201
renderer/src/components/main-panel/chat/options/setting.vue
Normal file
201
renderer/src/components/main-panel/chat/options/setting.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="chat-settings">
|
||||
<Model />
|
||||
<SystemPrompt />
|
||||
<ToolUse />
|
||||
<Websearch />
|
||||
<Temperature />
|
||||
<ContextLength />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, provide } from 'vue';
|
||||
import { llmManager } from '@/views/setting/llm';
|
||||
import { tabs } from '../../panel';
|
||||
import type { ChatSetting, ChatStorage } from '../chat';
|
||||
|
||||
import Model from './model.vue';
|
||||
import SystemPrompt from './system-prompt.vue';
|
||||
import ToolUse from './tool-use.vue';
|
||||
import Websearch from './websearch.vue';
|
||||
import Temperature from './temperature.vue';
|
||||
import ContextLength from './context-length.vue';
|
||||
|
||||
const props = defineProps({
|
||||
tabId: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ChatStorage & { settings: ChatSetting };
|
||||
|
||||
if (!tabStorage.settings) {
|
||||
tabStorage.settings = {
|
||||
modelIndex: llmManager.currentModelIndex,
|
||||
enableTools: [],
|
||||
enableWebSearch: false,
|
||||
temperature: 0.7,
|
||||
contextLength: 10,
|
||||
systemPrompt: ''
|
||||
} as ChatSetting;
|
||||
}
|
||||
|
||||
provide('tabStorage', tabStorage);
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.chat-settings {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 8px 0;
|
||||
background-color: var(--sidebar);
|
||||
width: fit-content;
|
||||
border-radius: 99%;
|
||||
bottom: 0px;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.setting-button {
|
||||
padding: 5px 8px;
|
||||
margin-right: 3px;
|
||||
border-radius: .5em;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.setting-button.active {
|
||||
background-color: var(--el-color-primary);
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.setting-button.active:hover {
|
||||
background-color: var(--el-color-primary);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.setting-button:hover {
|
||||
background-color: var(--background);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.value-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.slider-tips {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
/* 新增工具相关样式 */
|
||||
.tools-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.tool-info {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tool-description {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.tools-dialog-container .el-switch__core {
|
||||
border: 1px solid var(--main-color) !important;
|
||||
}
|
||||
|
||||
|
||||
.tools-dialog-container .el-switch .el-switch__action {
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
|
||||
.tools-dialog-container .el-switch.is-checked .el-switch__action {
|
||||
background-color: var(--sidebar);
|
||||
}
|
||||
|
||||
/* 新增工具对话框样式 */
|
||||
.tools-dialog-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.tools-list {
|
||||
flex: 1;
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.schema-viewer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.schema-viewer pre {
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
}
|
||||
|
||||
.schema-viewer .openmcp-code-block {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.schema-viewer code {
|
||||
font-family: var(--code-font-family);
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.badge-outer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge-inner {
|
||||
position: absolute;
|
||||
color: var(--foreground);
|
||||
background-color: var(--main-color);
|
||||
border-radius: 50%;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
top: -16px;
|
||||
right: -18px;
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,19 @@
|
||||
import { useMessageBridge } from "@/api/message-bridge";
|
||||
import { ref } from "vue";
|
||||
|
||||
interface SystemPrompt {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const systemPrompt = ref<SystemPrompt>({
|
||||
name: '默认',
|
||||
content: '你是一个AI助手, 你可以回答任何问题。'
|
||||
});
|
||||
|
||||
export function saveSystemPrompts() {
|
||||
const bridge = useMessageBridge();
|
||||
return new Promise(resolve => {
|
||||
|
||||
})
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<el-tooltip :content="t('system-prompt')" placement="top">
|
||||
<div class="setting-button" :class="{ 'active': hasSystemPrompt }" size="small"
|
||||
@click="showSystemPromptDialog = true">
|
||||
<span class="iconfont icon-prompt"></span>
|
||||
</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>
|
||||
<el-button @click="showSystemPromptDialog = false">{{ t("cancel") }}</el-button>
|
||||
<el-button type="primary" @click="showSystemPromptDialog = false">{{ t("save") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ChatStorage } from '../chat';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const tabStorage = inject('tabStorage') as ChatStorage;
|
||||
|
||||
const showSystemPromptDialog = ref(false);
|
||||
|
||||
const hasSystemPrompt = computed(() => {
|
||||
return !!tabStorage.settings.systemPrompt?.trim();
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<el-tooltip :content="t('temperature-parameter')" placement="top">
|
||||
<div class="setting-button" @click="showTemperatureSlider = true">
|
||||
<span class="iconfont icon-temperature"></span>
|
||||
<span class="value-badge">{{ tabStorage.settings.temperature.toFixed(1) }}</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 温度参数滑块 -->
|
||||
<el-dialog v-model="showTemperatureSlider" :title="t('temperature-parameter')" width="400px">
|
||||
<div class="slider-container">
|
||||
<el-slider v-model="tabStorage.settings.temperature" :min="0" :max="2" :step="0.1" />
|
||||
<div class="slider-tips">
|
||||
<span> {{ t('precise') }}(0)</span>
|
||||
<span>{{ t('moderate') }}(1)</span>
|
||||
<span>{{ t('creative') }}(2)</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showTemperatureSlider = false">{{ t("cancel") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ChatStorage } from '../chat';
|
||||
|
||||
const { t } = useI18n();
|
||||
const tabStorage = inject('tabStorage') as ChatStorage;
|
||||
|
||||
const showTemperatureSlider = ref(false);
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.icon-temperature {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
</style>
|
85
renderer/src/components/main-panel/chat/options/tool-use.vue
Normal file
85
renderer/src/components/main-panel/chat/options/tool-use.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<el-tooltip :content="t('tool-use')" placement="top">
|
||||
<div class="setting-button" :class="{ 'active': availableToolsNum > 0 }" size="small" @click="toggleTools">
|
||||
<span class="iconfont icon-tool badge-outer">
|
||||
<span class="badge-inner">
|
||||
{{ availableToolsNum }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</el-tooltip>
|
||||
|
||||
<el-dialog v-model="showToolsDialog" title="工具管理" width="800px">
|
||||
<div class="tools-dialog-container">
|
||||
<el-scrollbar height="400px" class="tools-list">
|
||||
<div v-for="(tool, index) in tabStorage.settings.enableTools" :key="index" class="tool-item">
|
||||
<div class="tool-info">
|
||||
<div class="tool-name">{{ tool.name }}</div>
|
||||
<div v-if="tool.description" class="tool-description">{{ tool.description }}</div>
|
||||
</div>
|
||||
<el-switch v-model="tool.enabled" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<el-scrollbar height="400px" class="schema-viewer">
|
||||
<div v-html="activeToolsSchemaHTML"></div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="enableAllTools">激活所有工具</el-button>
|
||||
<el-button type="danger" @click="disableAllTools">禁用所有工具</el-button>
|
||||
<el-button type="primary" @click="showToolsDialog = false">{{ t("cancel") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ChatStorage, getToolSchema } from '../chat';
|
||||
import { markdownToHtml } from '../markdown/markdown';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const tabStorage = inject('tabStorage') as ChatStorage;
|
||||
|
||||
const showToolsDialog = ref(false);
|
||||
|
||||
const availableToolsNum = computed(() => {
|
||||
return tabStorage.settings.enableTools.filter(tool => tool.enabled).length;
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 修改 toggleTools 方法
|
||||
const toggleTools = () => {
|
||||
showToolsDialog.value = true;
|
||||
};
|
||||
|
||||
const activeToolsSchemaHTML = computed(() => {
|
||||
const toolsSchema = getToolSchema(tabStorage.settings.enableTools);
|
||||
const jsonString = JSON.stringify(toolsSchema, null, 2);
|
||||
|
||||
return markdownToHtml(
|
||||
"```json\n" + jsonString + "\n```"
|
||||
);
|
||||
});
|
||||
|
||||
// 新增方法 - 激活所有工具
|
||||
const enableAllTools = () => {
|
||||
tabStorage.settings.enableTools.forEach(tool => {
|
||||
tool.enabled = true;
|
||||
});
|
||||
};
|
||||
|
||||
// 新增方法 - 禁用所有工具
|
||||
const disableAllTools = () => {
|
||||
tabStorage.settings.enableTools.forEach(tool => {
|
||||
tool.enabled = false;
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<el-tooltip :content="t('websearch')" placement="top">
|
||||
<div class="setting-button" :class="{ 'active': tabStorage.settings.enableWebSearch }" size="small"
|
||||
@click="toggleWebSearch">
|
||||
<span class="iconfont icon-web"></span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ChatStorage } from '../chat';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const tabStorage = inject('tabStorage') as ChatStorage;
|
||||
|
||||
const toggleWebSearch = () => {
|
||||
tabStorage.settings.enableWebSearch = !tabStorage.settings.enableWebSearch;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,438 +0,0 @@
|
||||
<template>
|
||||
<div class="chat-settings">
|
||||
<el-tooltip :content="t('choose-model')" placement="top">
|
||||
<div class="setting-button" @click="showModelDialog = true">
|
||||
<span class="iconfont icon-model">
|
||||
{{ currentServerName }}/{{ currentModelName }}
|
||||
</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="t('system-prompt')" placement="top">
|
||||
<div class="setting-button" :class="{ 'active': hasSystemPrompt }" size="small"
|
||||
@click="showSystemPromptDialog = true">
|
||||
<span class="iconfont icon-prompt"></span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="t('tool-use')" placement="top">
|
||||
<div class="setting-button" :class="{ 'active': availableToolsNum > 0 }" size="small"
|
||||
@click="toggleTools">
|
||||
<span class="iconfont icon-tool badge-outer">
|
||||
<span class="badge-inner">
|
||||
{{ availableToolsNum }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="t('websearch')" placement="top">
|
||||
<div class="setting-button" :class="{ 'active': tabStorage.settings.enableWebSearch }" size="small"
|
||||
@click="toggleWebSearch">
|
||||
<span class="iconfont icon-web"></span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip :content="t('temperature-parameter')" placement="top">
|
||||
<div class="setting-button" @click="showTemperatureSlider = true">
|
||||
<span class="iconfont icon-temperature"></span>
|
||||
<span class="value-badge">{{ tabStorage.settings.temperature.toFixed(1) }}</span>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 模型选择对话框 -->
|
||||
<el-dialog v-model="showModelDialog" :title="t('choose-model')" width="400px">
|
||||
<el-radio-group v-model="selectedModelIndex" @change="onRadioGroupChange">
|
||||
<el-radio v-for="(model, index) in availableModels" :key="index" :label="index">
|
||||
{{ model }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<template #footer>
|
||||
<el-button @click="showModelDialog = false">{{ t("cancel") }}</el-button>
|
||||
<el-button type="primary" @click="confirmModelChange">{{ t("confirm") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 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>
|
||||
<el-button @click="showSystemPromptDialog = false">{{ t("cancel") }}</el-button>
|
||||
<el-button type="primary" @click="showSystemPromptDialog = false">{{ t("save") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 温度参数滑块 -->
|
||||
<el-dialog v-model="showTemperatureSlider" :title="t('temperature-parameter')" width="400px">
|
||||
<div class="slider-container">
|
||||
<el-slider v-model="tabStorage.settings.temperature" :min="0" :max="2" :step="0.1" />
|
||||
<div class="slider-tips">
|
||||
<span> {{ t('precise') }}(0)</span>
|
||||
<span>{{ t('moderate') }}(1)</span>
|
||||
<span>{{ t('creative') }}(2)</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showTemperatureSlider = false">{{ t("cancel") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 上下文长度设置 - 改为滑块形式 -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showContextLengthDialog = false">{{ t("cancel") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 修改后的工具使用对话框 -->
|
||||
<el-dialog v-model="showToolsDialog" title="工具管理" width="800px">
|
||||
<div class="tools-dialog-container">
|
||||
<el-scrollbar height="400px" class="tools-list">
|
||||
<div v-for="(tool, index) in tabStorage.settings.enableTools" :key="index" class="tool-item">
|
||||
<div class="tool-info">
|
||||
<div class="tool-name">{{ tool.name }}</div>
|
||||
<div v-if="tool.description" class="tool-description">{{ tool.description }}</div>
|
||||
</div>
|
||||
<el-switch v-model="tool.enabled"/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<el-scrollbar height="400px" class="schema-viewer">
|
||||
<div v-html="activeToolsSchemaHTML"></div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="enableAllTools">激活所有工具</el-button>
|
||||
<el-button type="danger" @click="disableAllTools">禁用所有工具</el-button>
|
||||
<el-button type="primary" @click="showToolsDialog = false">{{ t("cancel") }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, defineProps, onMounted, onUnmounted } from 'vue';
|
||||
import { llmManager, llms } from '@/views/setting/llm';
|
||||
import { tabs } from '../panel';
|
||||
import { allTools, ChatSetting, ChatStorage, getToolSchema } from './chat';
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
import { CasualRestAPI, ToolItem, ToolsListResponse } from '@/hook/type';
|
||||
import { markdownToHtml } from './markdown';
|
||||
import { saveSetting } from '@/hook/setting';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
tabId: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const showModelDialog = ref(false);
|
||||
const showTemperatureSlider = ref(false);
|
||||
const showContextLengthDialog = ref(false);
|
||||
const showSystemPromptDialog = ref(false);
|
||||
|
||||
const currentServerName = computed(() => {
|
||||
const currentLlm = llms[llmManager.currentModelIndex];
|
||||
if (currentLlm) {
|
||||
return currentLlm.name;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const currentModelName = computed(() => {
|
||||
const currentLlm = llms[llmManager.currentModelIndex];
|
||||
if (currentLlm) {
|
||||
return currentLlm.models[selectedModelIndex.value];
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ChatStorage & { settings: ChatSetting };
|
||||
|
||||
|
||||
if (!tabStorage.settings) {
|
||||
tabStorage.settings = {
|
||||
modelIndex: llmManager.currentModelIndex,
|
||||
enableTools: [],
|
||||
enableWebSearch: false,
|
||||
temperature: 0.7,
|
||||
contextLength: 10,
|
||||
systemPrompt: ''
|
||||
} as ChatSetting;
|
||||
}
|
||||
|
||||
// 代表当前使用的服务商的当前模型的索引
|
||||
const currentModel = llms[llmManager.currentModelIndex].userModel;
|
||||
const selectedModelIndex = ref(llms[llmManager.currentModelIndex].models.indexOf(currentModel));
|
||||
|
||||
const availableModels = computed(() => {
|
||||
return llms[llmManager.currentModelIndex].models;
|
||||
});
|
||||
|
||||
const hasSystemPrompt = computed(() => {
|
||||
return !!tabStorage.settings.systemPrompt?.trim();
|
||||
});
|
||||
|
||||
|
||||
const showToolsDialog = ref(false);
|
||||
|
||||
const availableToolsNum = computed(() => {
|
||||
return tabStorage.settings.enableTools.filter(tool => tool.enabled).length;
|
||||
});
|
||||
|
||||
// 修改 toggleTools 方法
|
||||
const toggleTools = () => {
|
||||
showToolsDialog.value = true;
|
||||
};
|
||||
|
||||
|
||||
const toggleWebSearch = () => {
|
||||
tabStorage.settings.enableWebSearch = !tabStorage.settings.enableWebSearch;
|
||||
};
|
||||
|
||||
const confirmModelChange = () => {
|
||||
showModelDialog.value = false;
|
||||
};
|
||||
|
||||
const onRadioGroupChange = () => {
|
||||
const currentModel = llms[llmManager.currentModelIndex].models[selectedModelIndex.value];
|
||||
llms[llmManager.currentModelIndex].userModel = currentModel;
|
||||
saveSetting();
|
||||
};
|
||||
|
||||
const bridge = useMessageBridge();
|
||||
let commandCancel: (() => void);
|
||||
|
||||
onMounted(() => {
|
||||
commandCancel = bridge.addCommandListener('tools/list', (data: CasualRestAPI<ToolsListResponse>) => {
|
||||
allTools.value = data.msg.tools || [];
|
||||
tabStorage.settings.enableTools = [];
|
||||
for (const tool of allTools.value) {
|
||||
tabStorage.settings.enableTools.push({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}, { once: false });
|
||||
|
||||
bridge.postMessage({
|
||||
command: 'tools/list'
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (commandCancel) {
|
||||
commandCancel();
|
||||
}
|
||||
})
|
||||
|
||||
// 新增计算属性获取激活工具的JSON Schema
|
||||
const activeToolsSchemaHTML = computed(() => {
|
||||
const toolsSchema = getToolSchema(tabStorage.settings.enableTools);
|
||||
const jsonString = JSON.stringify(toolsSchema, null, 2);
|
||||
|
||||
return markdownToHtml(
|
||||
"```json\n" + jsonString + "\n```"
|
||||
);
|
||||
});
|
||||
|
||||
// 新增方法 - 激活所有工具
|
||||
const enableAllTools = () => {
|
||||
tabStorage.settings.enableTools.forEach(tool => {
|
||||
tool.enabled = true;
|
||||
});
|
||||
};
|
||||
|
||||
// 新增方法 - 禁用所有工具
|
||||
const disableAllTools = () => {
|
||||
tabStorage.settings.enableTools.forEach(tool => {
|
||||
tool.enabled = false;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.chat-settings {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 8px 0;
|
||||
background-color: var(--sidebar);
|
||||
width: fit-content;
|
||||
border-radius: 99%;
|
||||
bottom: 0px;
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.setting-button {
|
||||
padding: 5px 8px;
|
||||
margin-right: 3px;
|
||||
border-radius: .5em;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.setting-button.active {
|
||||
background-color: var(--el-color-primary);
|
||||
color: var(--el-text-color-primary);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.setting-button.active:hover {
|
||||
background-color: var(--el-color-primary);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.setting-button:hover {
|
||||
background-color: var(--background);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.value-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.icon-temperature {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.icon-length {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.slider-tips {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
/* 新增工具相关样式 */
|
||||
.tools-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.tool-info {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tool-description {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.tools-dialog-container .el-switch__core {
|
||||
border: 1px solid var(--main-color) !important;
|
||||
}
|
||||
|
||||
|
||||
.tools-dialog-container .el-switch .el-switch__action {
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
|
||||
.tools-dialog-container .el-switch.is-checked .el-switch__action {
|
||||
background-color: var(--sidebar);
|
||||
}
|
||||
|
||||
/* 新增工具对话框样式 */
|
||||
.tools-dialog-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.tools-list {
|
||||
flex: 1;
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.schema-viewer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.schema-viewer pre {
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
}
|
||||
|
||||
.schema-viewer .openmcp-code-block {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.schema-viewer code {
|
||||
font-family: var(--code-font-family);
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.badge-outer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge-inner {
|
||||
position: absolute;
|
||||
color: var(--foreground);
|
||||
background-color: var(--main-color);
|
||||
border-radius: 50%;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
top: -16px;
|
||||
right: -18px;
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
@ -75,7 +75,7 @@ onMounted(() => {
|
||||
|
||||
const targetResource = resourcesManager.templates.find(template => template.name === tabStorage.currentResourceName);
|
||||
if (targetResource === undefined) {
|
||||
tabStorage.currentResourceName = resourcesManager.templates[0].name;
|
||||
tabStorage.currentResourceName = resourcesManager.templates[0]?.name;
|
||||
tabStorage.lastResourceReadResponse = undefined;
|
||||
}
|
||||
}, { once: false });
|
||||
|
@ -40,7 +40,7 @@ import { defineComponent, defineProps, computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { tabs } from '../panel';
|
||||
import { ToolStorage } from './tools';
|
||||
import { markdownToHtml } from '../chat/markdown';
|
||||
import { markdownToHtml } from '../chat/markdown/markdown';
|
||||
|
||||
defineComponent({ name: 'tool-logger' });
|
||||
const { t } = useI18n();
|
||||
|
@ -23,25 +23,6 @@ export function getCurrentTime() {
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
|
||||
export function getBase64StringByFilename(filename: string) {
|
||||
const bridge = useMessageBridge();
|
||||
|
||||
return new Promise<string>(resolve => {
|
||||
bridge.addCommandListener('ocr/get-ocr-image', data => {
|
||||
const { code, msg = {} } = data;
|
||||
resolve(msg.base64String);
|
||||
}, { once: true});
|
||||
|
||||
bridge.postMessage({
|
||||
command: 'ocr/get-ocr-image',
|
||||
data: {
|
||||
filename
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const blobUrlCache = new Map<string, string>();
|
||||
|
||||
export async function getBlobUrlByFilename(filename: string) {
|
||||
@ -50,7 +31,13 @@ export async function getBlobUrlByFilename(filename: string) {
|
||||
return blobUrlCache.get(filename);
|
||||
}
|
||||
|
||||
const base64String = await getBase64StringByFilename(filename);
|
||||
const bridge = useMessageBridge();
|
||||
const res = await bridge.commandRequest('ocr/get-ocr-image', { filename });
|
||||
if (res?.code !== 200) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const base64String = res?.msg?.base64String;
|
||||
|
||||
if (!base64String) {
|
||||
return '';
|
||||
|
@ -4,7 +4,7 @@ import router from "./router";
|
||||
import i18n from './i18n';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import "@/components/main-panel/chat/prism/prism.css";
|
||||
import "@/components/main-panel/chat/markdown/prism/prism.css";
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
createApp(App)
|
||||
|
@ -72,6 +72,7 @@ export class ClientController {
|
||||
|
||||
@Controller('resources/templates/list')
|
||||
async listResourceTemplates(client: RequestClientType, data: any, webview: PostMessageble) {
|
||||
|
||||
if (!client) {
|
||||
return {
|
||||
code: 501,
|
||||
|
Loading…
x
Reference in New Issue
Block a user