433 lines
14 KiB
Vue
433 lines
14 KiB
Vue
<template>
|
||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" wrapClassName="ai-model-modal">
|
||
<div class="modal">
|
||
<div class="header">
|
||
<span class="header-title">
|
||
<span v-if="dataIndex ==='list' || dataIndex ==='add'" :class="dataIndex === 'list' ? '' : 'add-header-title pointer'" @click="goToList">选择供应商</span>
|
||
<span v-if="dataIndex === 'add'" class="add-header-title"> > </span>
|
||
<span v-if="dataIndex === 'add'" style="color: #1f2329">添加 {{ providerName }}</span>
|
||
</span>
|
||
|
||
<a-select v-if="dataIndex === 'list'" :bordered="false" class="header-select" size="small" v-model:value="modelType" @change="handleChange">
|
||
<a-select-option v-for="item in modelTypeOption" :value="item.value">{{ item.text }}</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
<div class="model-content" v-if="dataIndex === 'list'">
|
||
<a-row :span="24">
|
||
<a-col :xxl="12" :xl="12" :lg="12" :md="12" :sm="12" :xs="24" v-for="item in modelTypeList">
|
||
<a-card class="model-card" @click="handleClick(item)">
|
||
<div class="model-header">
|
||
<div class="flex">
|
||
<img :src="getImage(item.value)" class="header-img" />
|
||
<div class="header-text">{{ item.title }}</div>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
</a-col>
|
||
</a-row>
|
||
</div>
|
||
<a-tabs v-model:activeKey="activeKey" v-if="dataIndex === 'add' || dataIndex === 'edit'">
|
||
<a-tab-pane :key="1" tab="基础信息">
|
||
<div class="model-content">
|
||
<BasicForm @register="registerForm">
|
||
<template #modelType="{ model, field }">
|
||
<a-select v-model:value="model[field]" @change="handleModelTypeChange" :disabled="modelTypeDisabled">
|
||
<a-select-option v-for="item in modelTypeAddOption" :value="item">
|
||
<span v-if="item === 'LLM'">语言模型</span>
|
||
<span v-else>向量模型</span>
|
||
</a-select-option>
|
||
</a-select>
|
||
</template>
|
||
|
||
<template #modelName="{ model, field }">
|
||
<AutoComplete v-model:value="model[field]" :options="modelNameAddOption" :filter-option="filterOption">
|
||
<template #option="{ value, label, descr, type }">
|
||
<a-tooltip placement="right" color="#ffffff" :overlayInnerStyle="{ color:'#646a73' }">
|
||
<template #title>
|
||
<div v-html="getTitle(descr)"></div>
|
||
</template>
|
||
<div style="display: flex;justify-content: space-between;">
|
||
<span>{{label}}</span>
|
||
<div>
|
||
<a-tag v-if="type && type.indexOf('text') != -1" color="#E8D7C3">文本</a-tag>
|
||
<a-tag v-if="type && type.indexOf('image') != -1" color="#C3D9DC">图像分析</a-tag>
|
||
<a-tag v-if="type && type.indexOf('vector') != -1" color="#D4E0D8">向量</a-tag>
|
||
<a-tag v-if="type && type.indexOf('embeddings') != -1" color="#FFEBD3">文本嵌入</a-tag>
|
||
</div>
|
||
</div>
|
||
</a-tooltip>
|
||
</template>
|
||
</AutoComplete>
|
||
</template>
|
||
</BasicForm>
|
||
</div>
|
||
</a-tab-pane>
|
||
<a-tab-pane :key="2" tab="高级配置" v-if="modelParamsShow">
|
||
<AiModelSeniorForm ref="modelParamsRef" :modelParams="modelParams"></AiModelSeniorForm>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
|
||
</div>
|
||
<template v-if="dataIndex === 'add' || dataIndex === 'edit'" #footer>
|
||
<a-button @click="cancel">关闭</a-button>
|
||
<a-button @click="save" type="primary">保存</a-button>
|
||
</template>
|
||
<template v-else #footer> </template>
|
||
</BasicModal>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
import { ref, reactive } from 'vue';
|
||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||
import { useModal, useModalInner } from '@/components/Modal';
|
||
import { initDictOptions } from '@/utils/dict';
|
||
import model from './model.json';
|
||
import { AutoComplete } from 'ant-design-vue';
|
||
|
||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||
import { useForm } from '@/components/Form';
|
||
import { formSchema, imageList } from '../model.data';
|
||
import { editModel, queryById, saveModel } from '../model.api';
|
||
import { useMessage } from '/@/hooks/web/useMessage';
|
||
import AiModelSeniorForm from './AiModelSeniorForm.vue';
|
||
export default {
|
||
name: 'AddModelModal',
|
||
components: {
|
||
BasicForm,
|
||
BasicModal,
|
||
AiModelSeniorForm,
|
||
AutoComplete,
|
||
},
|
||
emits: ['success', 'register'],
|
||
setup(props, { emit }) {
|
||
//ai类型数据
|
||
const modelTypeData = ref<any>([]);
|
||
//模型类型下拉框
|
||
const modelTypeOption = ref<any>([]);
|
||
//模型类型禁用状态
|
||
const modelTypeDisabled = ref<boolean>(false);
|
||
//模型类型
|
||
const modelType = ref<string>('all');
|
||
//模型供应商
|
||
const modelTypeList = ref<any>([]);
|
||
//list:供应商选择页面,add 添加编辑
|
||
const dataIndex = ref<string>('list');
|
||
//供应商名称
|
||
const providerName = ref<string>('');
|
||
//添加模型类型的option
|
||
const modelTypeAddOption = ref<any>([]);
|
||
//添加模型名称的option
|
||
const modelNameAddOption = ref<any>([]);
|
||
//模型数据
|
||
const modelData = ref<any>({});
|
||
//tab切换对应的key
|
||
const activeKey = ref<number>(1);
|
||
//模型参数
|
||
const modelParams = ref<any>({});
|
||
//是否显示模型参数
|
||
const modelParamsShow = ref<boolean>(false);
|
||
//模型参数ref
|
||
const modelParamsRef = ref();
|
||
|
||
const getImage = (name) => {
|
||
return imageList.value[name];
|
||
};
|
||
//自动填充文本搜索事件
|
||
const filterOption = (input: string, option: any)=>{
|
||
return option.value.toUpperCase().indexOf(input.toUpperCase()) >= 0;
|
||
}
|
||
|
||
//表单配置
|
||
const [registerForm, { resetFields, setFieldsValue, validate, clearValidate }] = useForm({
|
||
schemas: formSchema,
|
||
showActionButtonGroup: false,
|
||
layout: 'vertical',
|
||
wrapperCol: { span: 24 },
|
||
});
|
||
|
||
//注册modal
|
||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||
activeKey.value = 1;
|
||
modelParamsShow.value = false;
|
||
if(dataIndex.value !== 'list') {
|
||
//重置表单
|
||
await resetFields();
|
||
}
|
||
setModalProps({ minHeight: 500 });
|
||
if (data.id) {
|
||
dataIndex.value = 'edit';
|
||
let values = await queryById({ id: data.id });
|
||
if (values) {
|
||
if(values.result.credential){
|
||
let credential = JSON.parse(values.result.credential);
|
||
if(credential.secretKey){
|
||
values.result.secretKey = credential.secretKey;
|
||
}
|
||
if(credential.apiKey){
|
||
values.result.apiKey = credential.apiKey;
|
||
}
|
||
}
|
||
let provider = values.result.provider;
|
||
let data = model.data.filter((item) => {
|
||
return item.value.includes(provider);
|
||
});
|
||
if (data && data.length > 0) {
|
||
modelTypeAddOption.value = data[0].type;
|
||
modelNameAddOption.value = data[0][values.result.modelType];
|
||
}
|
||
if(values.result.modelType && values.result.modelType === 'LLM'){
|
||
modelParamsShow.value = true;
|
||
}
|
||
if(values.result.modelParams){
|
||
modelParams.value = JSON.parse(values.result.modelParams)
|
||
}
|
||
modelTypeDisabled.value = true;
|
||
//表单赋值
|
||
await setFieldsValue({
|
||
...values.result,
|
||
});
|
||
//初始化模型提供者
|
||
initModelProvider();
|
||
}
|
||
} else {
|
||
modelTypeDisabled.value = false;
|
||
//初始化模型提供者
|
||
initModelProvider();
|
||
dataIndex.value = 'list';
|
||
modelNameAddOption.value = [];
|
||
}
|
||
});
|
||
|
||
//初始化模型类型
|
||
initModelTypeOption();
|
||
|
||
/**
|
||
* 初始化 模型类型字典
|
||
*/
|
||
function initModelTypeOption() {
|
||
initDictOptions('model_type').then((data) => {
|
||
modelTypeOption.value = data;
|
||
//update-begin---author:wangshuai---date:2025-03-04---for: 解决页面tab刷新一次就多一个全部类型的选项---
|
||
if(data[0].value != 'all'){
|
||
modelTypeOption.value.unshift({
|
||
text: '全部类型',
|
||
value: 'all',
|
||
});
|
||
}
|
||
//update-end---author:wangshuai---date:2025-03-04---for: 解决页面tab刷新一次就多一个全部类型的选项---
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 下拉框值选中事件
|
||
* @param value
|
||
*/
|
||
function handleChange(value) {
|
||
if ('all' == value) {
|
||
modelTypeList.value = model.data;
|
||
return;
|
||
}
|
||
let data = model.data.filter((item) => {
|
||
return item.type.includes(value);
|
||
});
|
||
modelTypeList.value = data;
|
||
}
|
||
|
||
/**
|
||
* 初始化模型提供者
|
||
*/
|
||
function initModelProvider() {
|
||
modelTypeList.value = model.data;
|
||
}
|
||
|
||
/**
|
||
* 供应商点击事件
|
||
*
|
||
* @param item
|
||
*/
|
||
function handleClick(item) {
|
||
dataIndex.value = 'add';
|
||
modelData.value = item;
|
||
providerName.value = item.title;
|
||
modelTypeAddOption.value = item.type;
|
||
setTimeout(()=>{
|
||
setFieldsValue({ 'provider': item.value, 'baseUrl': item.baseUrl })
|
||
},100)
|
||
}
|
||
|
||
/**
|
||
* 保存
|
||
*/
|
||
async function save() {
|
||
try {
|
||
setModalProps({ confirmLoading: true });
|
||
let values = await validate();
|
||
let credential = {
|
||
apiKey: values.apiKey,
|
||
secretKey: values.secretKey
|
||
}
|
||
if(modelParamsRef.value){
|
||
let modelParams = modelParamsRef.value.emitChange();
|
||
if(modelParams){
|
||
values.modelParams = JSON.stringify(modelParams);
|
||
}
|
||
}
|
||
values.credential = JSON.stringify(credential);
|
||
//新增
|
||
if (!values.id) {
|
||
values.provider = modelData.value.value;
|
||
await saveModel(values);
|
||
closeModal();
|
||
emit('success');
|
||
} else {
|
||
await editModel(values);
|
||
closeModal();
|
||
emit('success');
|
||
}
|
||
}catch(e){
|
||
if(e.hasOwnProperty('errorFields')){
|
||
activeKey.value = 1;
|
||
}
|
||
} finally {
|
||
setModalProps({ confirmLoading: false });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取消
|
||
*/
|
||
function cancel() {
|
||
dataIndex.value = 'list';
|
||
closeModal();
|
||
}
|
||
|
||
/**
|
||
* 模型类型选择事件
|
||
* @param value
|
||
*/
|
||
async function handleModelTypeChange(value) {
|
||
await setFieldsValue({ modelName: '' });
|
||
await clearValidate('modelName');
|
||
await setFieldsValue({
|
||
modelName: modelData.value[value+'DefaultValue']
|
||
})
|
||
modelNameAddOption.value = modelData.value[value];
|
||
if(value === 'LLM'){
|
||
modelParamsShow.value = true;
|
||
}else{
|
||
modelParamsShow.value = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 选择供应商
|
||
*/
|
||
function goToList() {
|
||
if (dataIndex.value === 'add') {
|
||
dataIndex.value = 'list';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取标题
|
||
* @param title
|
||
*/
|
||
function getTitle(title) {
|
||
if(!title){
|
||
return "暂无描述内容";
|
||
}
|
||
return title.replaceAll("\n","<br>")
|
||
}
|
||
|
||
return {
|
||
registerModal,
|
||
modelTypeData,
|
||
modelTypeOption,
|
||
modelType,
|
||
handleChange,
|
||
modelTypeList,
|
||
getImage,
|
||
handleClick,
|
||
dataIndex,
|
||
providerName,
|
||
save,
|
||
cancel,
|
||
registerForm,
|
||
handleModelTypeChange,
|
||
modelTypeAddOption,
|
||
modelNameAddOption,
|
||
goToList,
|
||
modelTypeDisabled,
|
||
activeKey,
|
||
modelParams,
|
||
modelParamsShow,
|
||
modelParamsRef,
|
||
filterOption,
|
||
getTitle,
|
||
};
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.modal {
|
||
padding: 12px 20px 20px 20px;
|
||
.header {
|
||
padding: 0 24px 24px 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
.header-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
}
|
||
.header-select {
|
||
margin-right: 10px;
|
||
}
|
||
.add-header-title {
|
||
color: #646a73;
|
||
}
|
||
}
|
||
.model-content {
|
||
.model-header {
|
||
position: relative;
|
||
font-size: 14px;
|
||
.header-img {
|
||
width: 32px;
|
||
height: 32px;
|
||
margin-right: 12px;
|
||
}
|
||
.header-text {
|
||
width: calc(100% - 80px);
|
||
overflow: hidden;
|
||
align-content: center;
|
||
}
|
||
}
|
||
}
|
||
.model-card {
|
||
margin-right: 10px;
|
||
margin-bottom: 10px;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
:deep(.ant-card .ant-card-body) {
|
||
padding: 12px;
|
||
}
|
||
|
||
.pointer {
|
||
cursor: pointer;
|
||
}
|
||
|
||
:deep(.jeecg-basic-modal-close){
|
||
span{
|
||
margin-left: 0 !important;
|
||
}
|
||
}
|
||
</style>
|
||
<style lang="less">
|
||
.ai-model-modal{
|
||
.jeecg-basic-modal-close > span{
|
||
margin-left: 0 !important;
|
||
}
|
||
}
|
||
</style>
|