重构AI配置组件以支持多个接口模型

This commit is contained in:
fofolee 2025-02-18 20:58:12 +08:00
parent 2217685072
commit 1be095fec2
5 changed files with 271 additions and 207 deletions

View File

@ -1,47 +1,28 @@
<template>
<div>
<div class="q-my-md">
<BorderLabel label="API配置">
<ButtonGroup
:model-value="argvs.apiConfig.modelType"
@update:modelValue="updateArgvs('apiConfig.modelType', $event)"
:options="modelTypeOptions"
height="26px"
class="q-mb-sm"
/>
<VariableInput
:model-value="argvs.apiConfig.apiUrl"
label="API地址"
:placeholder="
argvs.apiConfig.modelType === 'openai'
? '例https://api.openai.com/v1'
: '例http://localhost:11434'
"
@update:modelValue="updateArgvs('apiConfig.apiUrl', $event)"
class="q-mb-sm"
/>
<div class="row q-gutter-sm">
<VariableInput
class="col"
v-if="argvs.apiConfig.modelType === 'openai'"
:model-value="argvs.apiConfig.apiToken"
@update:modelValue="updateArgvs('apiConfig.apiToken', $event)"
label="API密钥"
/>
<VariableInput
class="col"
:model-value="argvs.apiConfig.model"
@update:modelValue="updateArgvs('apiConfig.model', $event)"
label="模型"
:placeholder="
argvs.apiConfig.modelType === 'openai'
? '例gpt-4o'
: '例qwen2.5:32b'
"
/>
<q-select
v-if="apiOptions.length > 0"
:model-value="argvs.apiConfig"
@update:model-value="updateArgvs('apiConfig', $event)"
:options="apiOptions"
map-options
emit-value
dense
options-dense
filled
label="API模型"
class="q-mb-sm"
/>
<q-field filled dense v-else class="q-mb-sm">
<template #control>
<div class="flex items-center justify-center full-width text-warning">
<q-icon name="warning" class="q-mr-sm" />
<div>
未配置API模型配置方法命令配置界面-右下角菜单按钮-API配置
</div>
</div>
</BorderLabel>
</div>
</template>
</q-field>
<ButtonGroup
:model-value="argvs.content.presetPrompt"
@update:modelValue="updateArgvs('content.presetPrompt', $event)"
@ -61,11 +42,11 @@
<script>
import { defineComponent } from "vue";
import BorderLabel from "components/composer/common/BorderLabel.vue";
import ButtonGroup from "components/composer/common/ButtonGroup.vue";
import { newVarInputVal } from "js/composer/varInputValManager";
import VariableInput from "components/composer/common/VariableInput.vue";
import { parseFunction, stringifyArgv } from "js/composer/formatString";
import { dbManager } from "js/utools.js";
export default defineComponent({
name: "AskAIEditor",
@ -74,7 +55,6 @@ export default defineComponent({
},
components: {
VariableInput,
BorderLabel,
ButtonGroup,
},
emits: ["update:modelValue"],
@ -87,6 +67,7 @@ export default defineComponent({
},
apiConfig: {},
},
apiOptions: [],
presetPromptOptions: [
{ label: "自由问答", value: "" },
{ label: "翻译", value: "translate" },
@ -111,17 +92,9 @@ export default defineComponent({
const argvs = window.lodashM.cloneDeep(this.defaultArgvs);
if (!code) return argvs;
try {
const variableFormatPaths = [
"arg0.prompt",
"arg1.apiUrl",
"arg1.apiToken",
"arg1.model",
];
const variableFormatPaths = ["arg0.prompt"];
const params = parseFunction(code, { variableFormatPaths });
return {
content: params.argvs[0],
apiConfig: params.argvs[1],
};
return params;
} catch (e) {
console.error("解析参数失败:", e);
}
@ -130,7 +103,7 @@ export default defineComponent({
generateCode(argvs = this.argvs) {
return `${this.modelValue.value}(${stringifyArgv(
argvs.content
)}, ${stringifyArgv(argvs.apiConfig)})`;
)}, ${JSON.stringify(argvs.apiConfig)})`;
},
getSummary(argvs) {
return "问AI" + argvs.content.prompt;
@ -153,14 +126,16 @@ export default defineComponent({
},
},
mounted() {
const aiConfig = this.$root.profile.aiConfig || {};
console.log("aiConfig", aiConfig);
this.defaultArgvs.apiConfig = {
modelType: aiConfig.modelType || "openai",
apiUrl: newVarInputVal("str", aiConfig.apiUrl || ""),
apiToken: newVarInputVal("str", aiConfig.apiToken || ""),
model: newVarInputVal("str", aiConfig.model || ""),
};
const apiConfigs = dbManager.getStorage("cfg_aiConfigs");
this.apiOptions = apiConfigs
? apiConfigs.map((config) => {
return {
label: config.name,
value: config,
};
})
: [];
this.defaultArgvs.apiConfig = apiConfigs?.[0] || {};
const argvs = this.modelValue.argvs || this.defaultArgvs;
if (!this.modelValue.code) {

View File

@ -2,7 +2,7 @@
<q-expansion-item
v-model="isExpanded"
@update:model-value="$emit('update:is-expanded', $event)"
class="command-composer command-config"
class="command-config"
expand-icon-toggle
>
<template v-slot:header>

View File

@ -1,54 +1,122 @@
<template>
<q-card style="width: 600px" class="q-pa-md">
<q-card-section class="text-h5"> API配置 </q-card-section>
<q-card-section>
<ButtonGroup
v-model="modelType"
:options="[
{ label: 'OPENAI', value: 'openai' },
{ label: 'OLLAMA', value: 'ollama' },
]"
/>
</q-card-section>
<q-card-section class="q-gutter-sm column">
<q-input outlined dense v-model="apiUrl">
<template v-slot:prepend>
<q-badge
color="primary"
text-color="black"
label="API地址"
class="q-pa-xs"
/>
</template>
</q-input>
<q-input outlined dense v-model="apiToken" v-if="modelType === 'openai'">
<template v-slot:prepend>
<q-badge
color="primary"
text-color="black"
label="API令牌"
class="q-pa-xs"
/>
</template>
</q-input>
<q-select
outlined
dense
v-model="model"
:options="models"
@focus="getModels"
<q-card style="width: 800px" class="q-pa-sm">
<div class="text-h5 q-my-md q-px-sm">API配置</div>
<div>
<div class="flex q-mb-md q-px-sm" style="height: 26px">
<ButtonGroup
v-model="modelToAdd"
class="col"
:options="[
{ label: 'OPENAI', value: 'openai' },
{ label: 'OLLAMA', value: 'ollama' },
]"
height="26px"
/>
<q-icon
name="add_box"
@click="addModel"
color="primary"
size="26px"
class="cursor-pointer q-ml-sm"
/>
</div>
<q-scroll-area
:style="`height: ${getConfigListHeight()}px;`"
class="q-px-sm"
:vertical-thumb-style="{
width: '2px',
}"
>
<template v-slot:prepend>
<q-badge
color="primary"
text-color="black"
label="模型名称"
class="q-pa-xs"
/>
</template>
</q-select>
</q-card-section>
<q-card-section class="flex justify-end q-gutter-sm">
<div class="config-list">
<div
v-for="(aiConfig, index) in aiConfigs"
:key="index"
class="config-item"
>
<div class="row q-col-gutter-sm">
<q-input
filled
dense
v-model="aiConfig.name"
class="col"
placeholder="请输入名称"
>
<template v-slot:prepend>
<q-badge
color="primary"
text-color="black"
label="名称"
class="q-pa-xs"
/>
</template>
<template v-slot:append>
<q-icon
color="grey"
name="remove_circle"
@click="deleteModel(index)"
size="16px"
class="cursor-pointer"
/>
</template>
</q-input>
<q-input
filled
dense
v-model="aiConfig.apiUrl"
class="col-8"
:placeholder="`${aiConfig.modelType} API地址`"
>
<template v-slot:prepend>
<q-badge
color="primary"
text-color="black"
label="接口"
class="q-pa-xs"
/>
</template>
</q-input>
</div>
<div class="row q-col-gutter-sm">
<q-select
filled
dense
v-model="aiConfig.model"
:options="models"
@focus="getModels(aiConfig)"
class="col"
>
<template v-slot:prepend>
<q-badge
color="primary"
text-color="black"
label="模型"
class="q-pa-xs"
/>
</template>
</q-select>
<q-input
filled
dense
v-model="aiConfig.apiToken"
v-if="aiConfig.modelType === 'openai'"
type="password"
class="col-8"
>
<template v-slot:prepend>
<q-badge
color="primary"
text-color="black"
label="令牌"
class="q-pa-xs"
/>
</template>
</q-input>
</div>
</div>
</div>
</q-scroll-area>
</div>
<div class="flex justify-end q-gutter-sm q-px-sm">
<q-btn flat color="grey" label="取消" v-close-popup />
<q-btn
flat
@ -57,12 +125,13 @@
v-close-popup
@click="saveConfig"
/>
</q-card-section>
</div>
</q-card>
</template>
<script>
import { defineComponent } from "vue";
import { dbManager } from "js/utools.js";
import ButtonGroup from "components/composer/common/ButtonGroup.vue";
export default defineComponent({
@ -72,42 +141,62 @@ export default defineComponent({
},
data() {
return {
modelType: "openai",
apiUrl: "",
apiToken: "",
model: "",
modelToAdd: "openai",
aiConfigs: [],
models: [],
};
},
methods: {
async getModels() {
try {
const { success, result } = await window.getModelsFromAiApi({
modelType: this.modelType,
apiUrl: this.apiUrl,
apiToken: this.apiToken,
});
this.models = success ? result : [];
} catch (_) {
this.models = [];
async getModels(aiConfig) {
const { success, result, error } = await window.getModelsFromAiApi(
aiConfig
);
if (!success) {
quickcommand.showMessageBox(error, "error");
return;
}
this.models = result;
},
saveConfig() {
this.$root.profile.aiConfig = {
modelType: this.modelType,
apiUrl: this.apiUrl,
apiToken: this.apiToken,
model: this.model,
};
console.log("saveConfig", this.$root.profile.aiConfig);
dbManager.setStorage(
"cfg_aiConfigs",
window.lodashM.cloneDeep(this.aiConfigs)
);
},
deleteModel(index) {
this.aiConfigs.splice(index, 1);
},
addModel() {
this.aiConfigs.push({
modelType: this.modelToAdd,
apiUrl: "",
apiToken: "",
model: "",
name: "",
});
},
getConfigListHeight() {
const counts = Math.min(this.aiConfigs.length, 3);
return counts * 100 + (counts - 1) * 8;
},
},
mounted() {
const aiConfig = this.$root.profile.aiConfig || {};
this.modelType = aiConfig.modelType || "openai";
this.apiUrl = aiConfig.apiUrl || "";
this.apiToken = aiConfig.apiToken || "";
this.model = aiConfig.model || "";
this.aiConfigs = dbManager.getStorage("cfg_aiConfigs") || [];
},
});
</script>
<style scoped>
.config-list,
.config-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.config-item {
border: 1px solid var(--q-primary);
border-radius: 4px;
padding: 8px;
}
</style>

View File

@ -368,3 +368,71 @@ body.body--dark.glass-effect-menu .q-menu {
transform: rotate(10deg);
}
}
/* q-field--filled 布局更加紧凑 */
/* 输入框高度及字体 */
.q-field--filled:not(.q-textarea) .q-field__control,
.q-field--filled:not(.q-textarea) .q-field__control>*,
.q-field--filled:not(.q-field--labeled):not(.q-textarea) .q-field__native {
max-height: 36px !important;
min-height: 36px !important;
}
.q-field--filled .q-field__control,
.q-field--filled .q-field__control>*,
.q-field--filled .q-field__native {
border-radius: 5px;
font-size: 12px;
}
.q-field--filled.q-select--with-chips .q-field__control .q-chip {
margin: 0 4px;
}
/* 输入框图标大小 */
.q-field--filled .q-field__control .q-icon {
font-size: 18px;
}
/* 输入框标签字体大小,占位时的位置 */
.q-field--filled .q-field__label {
font-size: 11px;
top: 11px;
}
/* 输入框标签悬浮的位置 */
.q-field--filled.q-field--dense.q-field--float .q-field__label {
transform: translateY(-50%) scale(0.75);
}
/* 去除filled输入框边框 */
.q-field--filled .q-field__control:before {
border: none;
}
/* 去除filled输入框下划线 */
.q-field--filled .q-field__control:after {
height: 0;
border-bottom: none;
}
/* 输入框背景颜色及内边距 */
.q-field--filled .q-field__control {
background: rgba(0, 0, 0, 0.03);
padding: 0 8px;
}
/* 输入框聚焦时的背景颜色 */
.q-field--filled.q-field--highlighted .q-field__control {
background: rgba(0, 0, 0, 0.03);
}
/* 暗黑模式下的输入框背景颜色 */
.body--dark .q-field--filled .q-field__control {
background: rgba(255, 255, 255, 0.04);
}
/* 暗黑模式下输入框聚焦时的背景颜色 */
.body--dark .q-field--filled.q-field--highlighted .q-field__control {
background: rgba(255, 255, 255, 0.08);
}

View File

@ -9,74 +9,6 @@
opacity: 0.8;
}
/* 布局更加紧凑 */
/* 输入框高度及字体 */
.command-composer .q-field--filled:not(.q-textarea) .q-field__control,
.command-composer .q-field--filled:not(.q-textarea) .q-field__control>*,
.command-composer .q-field--filled:not(.q-field--labeled):not(.q-textarea) .q-field__native {
max-height: 36px !important;
min-height: 36px !important;
}
.command-composer .q-field--filled .q-field__control,
.command-composer .q-field--filled .q-field__control>*,
.command-composer .q-field--filled .q-field__native {
border-radius: 5px;
font-size: 12px;
}
.command-composer .q-field--filled.q-select--with-chips .q-field__control .q-chip {
margin: 0 4px;
}
/* 输入框图标大小 */
.command-composer .q-field--filled .q-field__control .q-icon {
font-size: 18px;
}
/* 输入框标签字体大小,占位时的位置 */
.command-composer .q-field--filled .q-field__label {
font-size: 11px;
top: 11px;
}
/* 输入框标签悬浮的位置 */
.command-composer .q-field--filled.q-field--dense.q-field--float .q-field__label {
transform: translateY(-50%) scale(0.75);
}
/* 去除filled输入框边框 */
.command-composer .q-field--filled .q-field__control:before {
border: none;
}
/* 去除filled输入框下划线 */
.command-composer .q-field--filled .q-field__control:after {
height: 0;
border-bottom: none;
}
/* 输入框背景颜色及内边距 */
.command-composer .q-field--filled .q-field__control {
background: rgba(0, 0, 0, 0.03);
padding: 0 8px;
}
/* 输入框聚焦时的背景颜色 */
.command-composer .q-field--filled.q-field--highlighted .q-field__control {
background: rgba(0, 0, 0, 0.03);
}
/* 暗黑模式下的输入框背景颜色 */
.body--dark .command-composer .q-field--filled .q-field__control {
background: rgba(255, 255, 255, 0.04);
}
/* 暗黑模式下输入框聚焦时的背景颜色 */
.body--dark .command-composer .q-field--filled.q-field--highlighted .q-field__control {
background: rgba(255, 255, 255, 0.08);
}
/* checkbox/toggle大小及字体 */
.command-composer .q-checkbox__label,
.command-composer .q-toggle__label {