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

View File

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

View File

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