parent
eeb8d31f99
commit
2fd76aa5b9
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2024 LangChat. TyCoding All Rights Reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License, Version 3 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import request from '@/config/axios'
|
||||
|
||||
// AI 文档 VO
|
||||
export interface DocsVO {
|
||||
id: string
|
||||
// TODO: Add other fields based on your data model
|
||||
}
|
||||
|
||||
// AI 文档 API
|
||||
export const DocsApi = {
|
||||
// 获得文档分页
|
||||
async getDocsPage(params: any) {
|
||||
return await request.get({ url: '/aigc/docs/page', params })
|
||||
},
|
||||
|
||||
// 获得文档列表
|
||||
async getDocsList(params: any) {
|
||||
return await request.get({ url: '/aigc/docs/list', params })
|
||||
},
|
||||
|
||||
// 获得文档详情
|
||||
async getDocsById(id: string) {
|
||||
return await request.get({ url: `/aigc/docs/${id}` })
|
||||
},
|
||||
|
||||
// 创建文档
|
||||
async addDocs(data: any) {
|
||||
return await request.post({ url: '/aigc/docs', data })
|
||||
},
|
||||
|
||||
// 更新文档
|
||||
async updateDocs(data: any) {
|
||||
return await request.put({ url: '/aigc/docs', data })
|
||||
},
|
||||
|
||||
// 删除文档
|
||||
async deleteDocs(id: string) {
|
||||
return await request.delete({ url: `/aigc/docs/${id}` })
|
||||
},
|
||||
|
||||
// 重新向量化
|
||||
async reEmbedDocs(id: string) {
|
||||
return await request.get({ url: `/aigc/embedding/re-embed/${id}` })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (c) 2024 LangChat. TyCoding All Rights Reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License, Version 3 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import request from '@/config/axios'
|
||||
|
||||
// AI 嵌入存储 VO
|
||||
export interface EmbedStoreVO {
|
||||
id: string
|
||||
// TODO: Add other fields based on your data model
|
||||
}
|
||||
|
||||
// AI 嵌入存储 API
|
||||
export const EmbedStoreApi = {
|
||||
// 获得嵌入存储列表
|
||||
getEmbedStoreList: async (params: any) => {
|
||||
return await request.get({ url: '/aigc/embed-store/list', params })
|
||||
},
|
||||
|
||||
// 获得嵌入存储分页
|
||||
getEmbedStorePage: async (params: any) => {
|
||||
return await request.get({ url: '/aigc/embed-store/page', params })
|
||||
},
|
||||
|
||||
// 获得嵌入存储详情
|
||||
getEmbedStore: async (id: string) => {
|
||||
return await request.get({ url: `/aigc/embed-store/${id}` })
|
||||
},
|
||||
|
||||
// 创建嵌入存储
|
||||
createEmbedStore: async (data: any) => {
|
||||
return await request.post({ url: '/aigc/embed-store', data })
|
||||
},
|
||||
|
||||
// 更新嵌入存储
|
||||
updateEmbedStore: async (data: any) => {
|
||||
return await request.put({ url: '/aigc/embed-store', data })
|
||||
},
|
||||
|
||||
// 删除嵌入存储
|
||||
deleteEmbedStore: async (id: string) => {
|
||||
return await request.delete({ url: `/aigc/embed-store/${id}` })
|
||||
}
|
||||
}
|
||||
// export function list(params: any) {
|
||||
// return http.request({
|
||||
// url: '/aigc/embed-store/list',
|
||||
// method: 'get',
|
||||
// params,
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function page(params: any) {
|
||||
// return http.request({
|
||||
// url: '/aigc/embed-store/page',
|
||||
// method: 'get',
|
||||
// params,
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function getById(id: string) {
|
||||
// return http.request({
|
||||
// url: `/aigc/embed-store/${id}`,
|
||||
// method: 'get',
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function add(params: any) {
|
||||
// return http.request({
|
||||
// url: '/aigc/embed-store',
|
||||
// method: 'post',
|
||||
// params,
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function update(params: any) {
|
||||
// return http.request({
|
||||
// url: '/aigc/embed-store',
|
||||
// method: 'put',
|
||||
// params,
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function del(id?: string) {
|
||||
// return http.request({
|
||||
// url: `/aigc/embed-store/${id}`,
|
||||
// method: 'delete',
|
||||
// });
|
||||
// }
|
|
@ -0,0 +1,81 @@
|
|||
import request from '@/config/axios'
|
||||
import { AxiosProgressEvent } from 'axios'
|
||||
|
||||
// AI 嵌入 API
|
||||
export const EmbeddingApi = {
|
||||
// 文本嵌入
|
||||
embeddingText: async (params: any) => {
|
||||
return await request.post({ url: '/aigc/embedding/text', params })
|
||||
},
|
||||
|
||||
// 嵌入搜索
|
||||
embeddingSearch: async (data: any) => {
|
||||
return await request.post({ url: '/aigc/embedding/search', data })
|
||||
},
|
||||
|
||||
// 文档嵌入
|
||||
embeddingDocs: async (
|
||||
knowledgeId: string,
|
||||
data: any,
|
||||
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
) => {
|
||||
return await request.post({
|
||||
url: `/aigc/embedding/docs/${knowledgeId}`,
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress
|
||||
})
|
||||
}
|
||||
}
|
||||
// /*
|
||||
// * Copyright (c) 2024 LangChat. TyCoding All Rights Reserved.
|
||||
// *
|
||||
// * Licensed under the GNU Affero General Public License, Version 3 (the "License");
|
||||
// * you may not use this file except in compliance with the License.
|
||||
// * You may obtain a copy of the License at
|
||||
// *
|
||||
// * https://www.gnu.org/licenses/agpl-3.0.html
|
||||
// *
|
||||
// * Unless required by applicable law or agreed to in writing, software
|
||||
// * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// * See the License for the specific language governing permissions and
|
||||
// * limitations under the License.
|
||||
// */
|
||||
|
||||
// import { http } from '@/utils/http/axios';
|
||||
// import { AxiosProgressEvent } from 'axios';
|
||||
|
||||
// export function embeddingText(params: any) {
|
||||
// return http.request({
|
||||
// url: '/aigc/embedding/text',
|
||||
// method: 'post',
|
||||
// params,
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function embeddingSearch(data: any) {
|
||||
// return http.request({
|
||||
// url: '/aigc/embedding/search',
|
||||
// method: 'post',
|
||||
// data,
|
||||
// });
|
||||
// }
|
||||
|
||||
// export function embeddingDocs(
|
||||
// knowledgeId: string,
|
||||
// data: any,
|
||||
// onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||
// ) {
|
||||
// return http.request({
|
||||
// url: `/aigc/embedding/docs/${knowledgeId}`,
|
||||
// method: 'post',
|
||||
// data,
|
||||
// headers: {
|
||||
// 'Content-Type': 'multipart/form-data',
|
||||
// },
|
||||
// onUploadProgress,
|
||||
// });
|
||||
// }
|
|
@ -0,0 +1,40 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// AI 知识库 VO
|
||||
export interface KnowledgeVO {
|
||||
id: string
|
||||
// TODO: Add other fields based on your data model
|
||||
}
|
||||
|
||||
// AI 知识库 API
|
||||
export const KnowledgeApi = {
|
||||
// 获得知识库列表
|
||||
getKnowledgeList: async (params: any) => {
|
||||
return await request.get({ url: '/aigc/knowledge/list', params })
|
||||
},
|
||||
|
||||
// 获得知识库分页
|
||||
getKnowledgePage: async (params: any) => {
|
||||
return await request.get({ url: '/aigc/knowledge/page', params })
|
||||
},
|
||||
|
||||
// 获得知识库详情
|
||||
getKnowledge: async (id: string) => {
|
||||
return await request.get({ url: `/aigc/knowledge/${id}` })
|
||||
},
|
||||
|
||||
// 创建知识库
|
||||
createKnowledge: async (data: any) => {
|
||||
return await request.post({ url: '/aigc/knowledge', data })
|
||||
},
|
||||
|
||||
// 更新知识库
|
||||
updateKnowledge: async (data: any) => {
|
||||
return await request.put({ url: '/aigc/knowledge', data })
|
||||
},
|
||||
|
||||
// 删除知识库
|
||||
deleteKnowledge: async (id: string) => {
|
||||
return await request.delete({ url: `/aigc/knowledge/${id}` })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// AI 模型 VO
|
||||
export interface ModelVO {
|
||||
id: string
|
||||
// TODO: Add other fields based on your data model
|
||||
}
|
||||
|
||||
// AI 模型 API
|
||||
export const ModelApi = {
|
||||
// 获得模型分页
|
||||
getModelPage: async (params: any) => {
|
||||
return await request.get({ url: '/chat/aigc/model/page', params })
|
||||
},
|
||||
|
||||
// 获得模型列表
|
||||
getModelList: async (params: any) => {
|
||||
return await request.get({ url: '/chat/aigc/model/list', params })
|
||||
},
|
||||
|
||||
// 获得模型详情
|
||||
getModel: async (id: string) => {
|
||||
return await request.get({ url: `/chat/aigc/model/${id}` })
|
||||
},
|
||||
|
||||
// 创建模型
|
||||
createModel: async (data: any) => {
|
||||
return await request.post({ url: '/chat/aigc/model', data })
|
||||
},
|
||||
|
||||
// 更新模型
|
||||
updateModel: async (data: any) => {
|
||||
return await request.put({ url: '/chat/aigc/model', data })
|
||||
},
|
||||
|
||||
// 删除模型
|
||||
deleteModel: async (id: string) => {
|
||||
return await request.delete({ url: `/chat/aigc/model/${id}` })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2024 LangChat. TyCoding All Rights Reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License, Version 3 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.gnu.org/licenses/agpl-3.0.html
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import request from '@/config/axios'
|
||||
|
||||
// AI 文档切片 VO
|
||||
export interface SliceVO {
|
||||
id: string
|
||||
// TODO: Add other fields based on your data model
|
||||
}
|
||||
|
||||
// AI 文档切片 API
|
||||
export const SliceApi = {
|
||||
// 获得切片分页
|
||||
getSlicePage: async (params: any) => {
|
||||
return await request.get({ url: '/aigc/docs/slice/page', params })
|
||||
},
|
||||
|
||||
// 获得切片列表
|
||||
getSliceList: async (params: any) => {
|
||||
return await request.get({ url: '/aigc/docs/slice/list', params })
|
||||
},
|
||||
|
||||
// 获得切片详情
|
||||
getSlice: async (id: string) => {
|
||||
return await request.get({ url: `/aigc/docs/slice/${id}` })
|
||||
},
|
||||
|
||||
// 创建切片
|
||||
createSlice: async (data: any) => {
|
||||
return await request.post({ url: '/aigc/docs/slice', data })
|
||||
},
|
||||
|
||||
// 更新切片
|
||||
updateSlice: async (data: any) => {
|
||||
return await request.put({ url: '/aigc/docs/slice', data })
|
||||
},
|
||||
|
||||
// 删除切片
|
||||
deleteSlice: async (id: string) => {
|
||||
return await request.delete({ url: `/aigc/docs/slice/${id}` })
|
||||
}
|
||||
}
|
|
@ -230,6 +230,7 @@ export default defineComponent({
|
|||
return slots[item.field] ? (
|
||||
getSlot(slots, item.field, formModel.value)
|
||||
) : (
|
||||
<>
|
||||
<Com
|
||||
vModel={formModel.value[item.field]}
|
||||
{...(autoSetPlaceholder && setTextPlaceholder(item))}
|
||||
|
@ -242,6 +243,8 @@ export default defineComponent({
|
|||
>
|
||||
{{ ...slotsMap }}
|
||||
</Com>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -81,5 +81,9 @@ export function getColumns(provider: string) {
|
|||
return zhipuColumns;
|
||||
}
|
||||
}
|
||||
return openaiColumns;
|
||||
return [...openaiColumns, {
|
||||
label: '操作',
|
||||
field: 'action',
|
||||
width: '150',
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -196,3 +196,9 @@ export const LLMProviders: any[] = [
|
|||
models: ['claude-3-opus', 'claude-3-opus-20240229', 'claude-3-sonnet', 'claude-3-haiku'],
|
||||
},
|
||||
];
|
||||
export enum ModelTypeEnum {
|
||||
CHAT = 'CHAT',
|
||||
EMBEDDING = 'EMBEDDING',
|
||||
TEXT_IMAGE = 'TEXT_IMAGE',
|
||||
WEB_SEARCH = 'WEB_SEARCH',
|
||||
}
|
|
@ -1,25 +1,55 @@
|
|||
<script setup lang="ts">
|
||||
import {Form} from '@/components/Form'
|
||||
import {ref} from 'vue'
|
||||
import {nextTick, ref} from 'vue'
|
||||
import {FormSchema} from "@/types/form";
|
||||
import {getSchemas} from "@/views/ai/model/chatModel/composables/schemas";
|
||||
import {ModelApi} from '@/api/new-ai/model';
|
||||
import {ElMessage} from 'element-plus';
|
||||
|
||||
const emit = defineEmits(['reload'])
|
||||
const visible = ref(false)
|
||||
const formData = ref({})
|
||||
const formRef = ref()
|
||||
const schemas = ref([])
|
||||
const isEdit = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const close = () => {
|
||||
formData.value = {}
|
||||
visible.value = false
|
||||
formRef.value.clearForm()
|
||||
schemas.value = []
|
||||
nextTick(() => {
|
||||
formRef.value?.clearForm()
|
||||
formRef.value?.setSchema(schemas.value)
|
||||
})
|
||||
}
|
||||
const show = async (data: object) => {
|
||||
|
||||
const show = async (data: any = {}) => {
|
||||
visible.value = true
|
||||
isEdit.value = !!data.id
|
||||
await nextTick()
|
||||
formRef.value.setValues(data)
|
||||
schemas.value = (getSchemas(data.provider) as FormSchema[]).splice(1);
|
||||
schemas.value = getSchemas(data.provider).slice(1);
|
||||
formRef.value.setSchema(schemas.value)
|
||||
// models.value = getModels(data.provider, LLMProviders)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
|
||||
const form = formRef.value.getElFormRef()
|
||||
await form.validate()
|
||||
const values = formRef.value.formModel
|
||||
loading.value = true
|
||||
const api = isEdit.value ? ModelApi.updateModel : ModelApi.createModel
|
||||
await api(values).finally(() => loading.value = false)
|
||||
ElMessage.success(isEdit.value ? '更新模型成功' : '创建模型成功');
|
||||
close();
|
||||
emit('reload');
|
||||
} catch (error) {
|
||||
console.error('Failed to save model:', error);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
close
|
||||
|
@ -27,15 +57,14 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="visible" draggable title="编辑" width="800px" @close="close">
|
||||
<el-dialog :close-on-click-modal="false" :close-on-press-escape="false" v-model="visible" draggable :title="isEdit ? '编辑模型' : '新增模型'" width="800px" @close="close">
|
||||
<Form ref="formRef" :model="formData" :schema="schemas"/>
|
||||
<template #footer>
|
||||
<el-button type="primary">确认</el-button>
|
||||
<el-button @click="close">取消</el-button>
|
||||
<el-button :loading="loading" type="primary" @click="handleSubmit">确认</el-button>
|
||||
<el-button :loading="loading" @click="close">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
|
|
|
@ -3,14 +3,12 @@
|
|||
import {Table} from '@/components/Table';
|
||||
import {Delete, Edit, Plus} from '@element-plus/icons-vue';
|
||||
import editCom from './edit.vue';
|
||||
import {computed, h, nextTick, reactive, ref} from 'vue';
|
||||
import {getColumns} from './composables/columns.ts';
|
||||
import {LLMProviders} from './composables/consts.ts';
|
||||
// import { del, list as getModels } from '@/api/aigc/model';
|
||||
import {computed, h, nextTick, reactive, ref, watch, onMounted} from 'vue';
|
||||
import {getColumns} from './composables/columns';
|
||||
import {LLMProviders, ModelTypeEnum} from './composables/consts';
|
||||
import {ModelApi} from '@/api/new-ai/model';
|
||||
import {ElMessage, ElMessageBox} from 'element-plus';
|
||||
import {FormSchema} from "@/types/form";
|
||||
// import { ModelTypeEnum } from '@/api/models';
|
||||
import {getModels, ProviderEnum} from './composables/provider.ts';
|
||||
import {getModels, ProviderEnum} from './composables/provider';
|
||||
|
||||
const formData = ref({
|
||||
provider: ProviderEnum.OPENAI
|
||||
|
@ -19,107 +17,74 @@ const message = ElMessage;
|
|||
const dialog = ElMessageBox;
|
||||
const actionRef = ref();
|
||||
const editRef = ref();
|
||||
const tableData = ref([
|
||||
{
|
||||
name: '1111'
|
||||
},
|
||||
{
|
||||
name: '1111'
|
||||
},
|
||||
{
|
||||
name: '1111'
|
||||
},
|
||||
{
|
||||
name: '1111'
|
||||
},
|
||||
{
|
||||
name: '1111'
|
||||
},
|
||||
{
|
||||
name: '1111'
|
||||
},
|
||||
{
|
||||
name: '1111'
|
||||
},
|
||||
{
|
||||
name: '1111'
|
||||
}, {
|
||||
name: '1111'
|
||||
},
|
||||
{
|
||||
name: '1111'
|
||||
const tableData = ref([]);
|
||||
const loading = ref(false);
|
||||
// 获取模型列表
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await ModelApi.getModelList({
|
||||
provider: formData.value.provider,
|
||||
type: ModelTypeEnum.CHAT
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
console.log(res)
|
||||
tableData.value = res;
|
||||
} catch (error) {
|
||||
console.error('Failed to load models:', error);
|
||||
message.error('获取模型列表失败');
|
||||
}
|
||||
])
|
||||
// const actionColumn = reactive({
|
||||
// width: 100,
|
||||
// title: '操作',
|
||||
// key: 'action',
|
||||
// fixed: 'right',
|
||||
// align: 'center',
|
||||
// render(record: any) {
|
||||
// return h(TableAction as any, {
|
||||
// style: 'text',
|
||||
// actions: [
|
||||
// {
|
||||
// type: 'info',
|
||||
// icon: Edit,
|
||||
// onClick: handleEdit.bind(null, record),
|
||||
// },
|
||||
// {
|
||||
// type: 'error',
|
||||
// icon: Delete,
|
||||
// onClick: handleDel.bind(null, record),
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
};
|
||||
|
||||
// 监听 provider 变化重新加载数据
|
||||
watch(() => formData.value.provider, () => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
// 初始加载
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
const columns = computed(() => {
|
||||
nextTick();
|
||||
return getColumns(formData.value.provider);
|
||||
});
|
||||
// const loadDataTable = async (params: any) => {
|
||||
// if (formData.value.provider === '') {
|
||||
// formData.value.provider = LLMProviders[0].model;
|
||||
// }
|
||||
// return await getModels({ ...params, provider: formData.value.provider, type: ModelTypeEnum.CHAT });
|
||||
// };
|
||||
|
||||
async function addModel() {
|
||||
console.log(formData.value.provider);
|
||||
editRef.value.show({provider: formData.value.provider});
|
||||
editRef.value.show({provider: formData.value.provider, type: ModelTypeEnum.CHAT });
|
||||
}
|
||||
|
||||
function handleEdit(record: any) {
|
||||
editRef.value.show(record);
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
actionRef.value.reload();
|
||||
async function reloadTable() {
|
||||
await loadData();
|
||||
}
|
||||
|
||||
function handleDel(record: any) {
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
message: `你确定删除 [${record.name}] 模型吗?删除之后不可再用该模型对话`,
|
||||
async function handleDel(record: any) {
|
||||
dialog.confirm('确定要删除该模型吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '不确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
await del(record.id);
|
||||
reloadTable();
|
||||
try {
|
||||
await ModelApi.deleteModel(record.id);
|
||||
await reloadTable();
|
||||
message.success('模型删除成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete model:', error);
|
||||
message.error('删除模型失败');
|
||||
}
|
||||
}).catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <content-wrap>-->
|
||||
<!-- <el-button v-for="(item,index) in LLMProviders" :key="index" @click="formData.provider = item.model">{{ item.name }}</el-button>-->
|
||||
<!-- </content-wrap>-->
|
||||
<ContentWrap>
|
||||
<div class="flex children">
|
||||
<el-scrollbar class="h-full w-300px pl-10px pr-20px">
|
||||
|
@ -130,7 +95,7 @@ function handleDel(record: any) {
|
|||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div class="h-full flex-1 px-20px">
|
||||
<div class="h-full flex-1 px-20px" v-loading="loading">
|
||||
<el-alert
|
||||
class="w-full mb-10px min-alert"
|
||||
title="对于完全适配OpenAI接口格式的模型都可在OpenAI中配置(只需要定义BaseUrl)"
|
||||
|
@ -138,7 +103,12 @@ function handleDel(record: any) {
|
|||
show-icon
|
||||
/>
|
||||
<el-button :icon="Plus" class="my-10px" type="primary" @click="addModel">新增模型</el-button>
|
||||
<Table class="table-wrapper" height="100%" border :columns="columns" :data="tableData.concat(tableData)" :pagination="false"/>
|
||||
<Table class="table-wrapper" height="100%" border :columns="columns" :data="tableData" :pagination="false" >
|
||||
<template #action="{row}">
|
||||
<el-button text :icon="Edit" @click="handleEdit(row)"/>
|
||||
<el-button text :icon="Delete" type="danger" @click="handleDel(row)"/>
|
||||
</template>
|
||||
</Table>
|
||||
<editCom ref="editRef" @reload="reloadTable"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import {ref} from "vue";
|
||||
import {FormSchema} from "@/types/form";
|
||||
import {ElTag} from "element-plus";
|
||||
import {EmbedStoreApi} from "@/api/new-ai/embed-store";
|
||||
|
||||
export enum ProviderEnum {
|
||||
Redis = 'REDIS',
|
||||
PgVector = 'PGVECTOR',
|
||||
Milvus = 'MILVUS',
|
||||
}
|
||||
|
||||
|
||||
export const ProviderConst = [
|
||||
{ label: 'Redis', value: ProviderEnum.Redis },
|
||||
{ label: 'PgVector', value: ProviderEnum.PgVector },
|
||||
{ label: 'Milvus', value: ProviderEnum.Milvus },
|
||||
];
|
||||
export default function () {
|
||||
const ProviderConst = [
|
||||
{ label: 'Redis', value: ProviderEnum.Redis },
|
||||
{ label: 'PgVector', value: ProviderEnum.PgVector },
|
||||
{ label: 'Milvus', value: ProviderEnum.Milvus },
|
||||
];
|
||||
|
||||
function getProviderLabel(value: any) {
|
||||
const arr = ProviderConst.filter((i) => i.value === value);
|
||||
if (arr === undefined || arr.length === 0) {
|
||||
|
@ -22,6 +27,7 @@ export default function () {
|
|||
}
|
||||
return arr[0].label;
|
||||
}
|
||||
|
||||
const shema = ref<FormSchema[]>([
|
||||
{
|
||||
label: '模型名称',
|
||||
|
@ -32,6 +38,7 @@ export default function () {
|
|||
},
|
||||
}
|
||||
])
|
||||
|
||||
const columns = ref<object[]>([
|
||||
{
|
||||
label: '数据库别名',
|
||||
|
@ -59,7 +66,7 @@ export default function () {
|
|||
label: '向量纬度',
|
||||
field: 'dimension',
|
||||
align: 'center',
|
||||
width: '80',
|
||||
width: '100',
|
||||
render(row) {
|
||||
return h(
|
||||
ElTag,
|
||||
|
@ -76,7 +83,6 @@ export default function () {
|
|||
label: '数据库地址',
|
||||
field: 'host',
|
||||
align: 'center',
|
||||
width: '110',
|
||||
},
|
||||
{
|
||||
label: '数据库端口',
|
||||
|
@ -85,32 +91,46 @@ export default function () {
|
|||
width: '100',
|
||||
},
|
||||
{
|
||||
label: '数据库用户名',
|
||||
field: 'username',
|
||||
label: '数据库名称',
|
||||
field: 'database',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: '数据库密码',
|
||||
field: 'password',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: '数据库名',
|
||||
field: 'databaseName',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: '表名称',
|
||||
field: 'tableName',
|
||||
align: 'center',
|
||||
},
|
||||
])
|
||||
]);
|
||||
|
||||
const tableData = ref([])
|
||||
const editRef = ref()
|
||||
const searchParams = ref({})
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await EmbedStoreApi.getEmbedStorePage(searchParams.value);
|
||||
tableData.value = res.data.list;
|
||||
} catch (error) {
|
||||
console.error('Failed to load embed stores:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑对话框
|
||||
const open = () => {
|
||||
editRef.value.show({});
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (values: any) => {
|
||||
searchParams.value = values;
|
||||
loadData();
|
||||
}
|
||||
|
||||
return {
|
||||
ProviderConst,
|
||||
getProviderLabel,
|
||||
shema,
|
||||
columns,
|
||||
tableData
|
||||
tableData,
|
||||
editRef,
|
||||
open,
|
||||
loadData,
|
||||
handleSearch,
|
||||
ProviderConst,
|
||||
getProviderLabel
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
<script setup lang="ts">
|
||||
import {Form} from '@/components/Form'
|
||||
import {nextTick, ref} from 'vue'
|
||||
import {FormSchema} from "@/types/form";
|
||||
import {EmbedStoreApi} from "@/api/new-ai/embed-store";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {ProviderConst} from './composables'
|
||||
|
||||
const emit = defineEmits(['reload'])
|
||||
const visible = ref(false)
|
||||
const formData = ref({})
|
||||
const formRef = ref()
|
||||
const isEdit = ref(false)
|
||||
|
||||
const schemas = ref<FormSchema[]>([
|
||||
{
|
||||
field: 'name',
|
||||
label: '数据库别名',
|
||||
component: 'Input',
|
||||
formItemProps: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'provider',
|
||||
label: '供应商',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: ProviderConst,
|
||||
},
|
||||
formItemProps: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'dimension',
|
||||
label: '向量纬度',
|
||||
component: 'InputNumber',
|
||||
formItemProps: {
|
||||
required: true,
|
||||
},
|
||||
componentProps: {
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
label: '数据库地址',
|
||||
component: 'Input',
|
||||
formItemProps: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'port',
|
||||
label: '数据库端口',
|
||||
component: 'InputNumber',
|
||||
formItemProps: {
|
||||
required: true,
|
||||
},
|
||||
componentProps: {
|
||||
min: 1,
|
||||
max: 65535,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'database',
|
||||
label: '数据库名称',
|
||||
component: 'Input',
|
||||
formItemProps: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'username',
|
||||
label: '数据库用户名',
|
||||
component: 'Input',
|
||||
formItemProps: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'password',
|
||||
label: '数据库密码',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
type: 'password',
|
||||
showPassword: true,
|
||||
},
|
||||
formItemProps: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
formRef.value.clearForm()
|
||||
}
|
||||
|
||||
const show = async (data: any = {}) => {
|
||||
visible.value = true
|
||||
isEdit.value = !!data.id
|
||||
await nextTick()
|
||||
formRef.value.setValues(data)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const form = formRef.value.getElFormRef()
|
||||
await form.validate()
|
||||
const values = formRef.value.formModel
|
||||
|
||||
if (isEdit.value) {
|
||||
await EmbedStoreApi.updateEmbedStore(values);
|
||||
ElMessage.success('更新向量数据库成功');
|
||||
} else {
|
||||
await EmbedStoreApi.createEmbedStore(values);
|
||||
ElMessage.success('创建向量数据库成功');
|
||||
}
|
||||
close();
|
||||
emit('reload');
|
||||
} catch (error) {
|
||||
console.error('Failed to save embed store:', error);
|
||||
ElMessage.error(isEdit.value ? '更新向量数据库失败' : '创建向量数据库失败');
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
close
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="visible" draggable :title="isEdit ? '编辑向量数据库' : '新增向量数据库'" width="800px" @close="close">
|
||||
<Form ref="formRef" :model="formData" :schema="schemas" />
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="handleSubmit">确认</el-button>
|
||||
<el-button @click="close">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
|
@ -1,13 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import {Table} from "@/components/Table";
|
||||
import {Search} from "@/components/Search";
|
||||
import useEmbedStore from './composables'
|
||||
const { shema, columns, tableData } = useEmbedStore()
|
||||
import Edit from './edit.vue'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const { shema, columns, tableData, editRef, open, loadData, handleSearch } = useEmbedStore()
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<content-wrap>
|
||||
<Search :schema="shema"/>
|
||||
<Search :schema="shema" @search="handleSearch" @reset="handleSearch"/>
|
||||
</content-wrap>
|
||||
<ContentWrap>
|
||||
<el-alert
|
||||
|
@ -16,8 +24,9 @@ const { shema, columns, tableData } = useEmbedStore()
|
|||
type="info"
|
||||
show-icon
|
||||
/>
|
||||
<el-button class="my-10px" type="primary" :icon="Plus">新增向量数据库</el-button>
|
||||
<Table height="cacl(100% - 400px)" border :columns="columns" :data="tableData.concat(tableData)" :pagination="false"/>
|
||||
<el-button class="my-10px" type="primary" :icon="Plus" @click="open">新增向量数据库</el-button>
|
||||
<Table height="calc(100% - 400px)" border :columns="columns" :data="tableData" :pagination="false"/>
|
||||
<Edit ref="editRef" @reload="loadData" />
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import {ElTag} from "element-plus";
|
||||
import {ref} from "vue";
|
||||
import {ref, watch} from "vue";
|
||||
import {ProviderEnum} from "@/views/ai/model/chatModel/composables/provider";
|
||||
import {ModelApi} from "@/api/new-ai/model";
|
||||
import {ElTag, ElMessageBox as dialog, ElMessage as message} from "element-plus";
|
||||
import { ModelTypeEnum } from "../../chatModel/composables/consts";
|
||||
|
||||
|
||||
export default function () {
|
||||
const formData = ref({
|
||||
provider: ProviderEnum.OPENAI
|
||||
})
|
||||
const editRef = ref()
|
||||
const formData = ref({
|
||||
provider: ProviderEnum.OPENAI,
|
||||
type: ModelTypeEnum.EMBEDDING
|
||||
});
|
||||
const tableData = ref([])
|
||||
|
||||
const baseColumns = [
|
||||
{
|
||||
label: '模型别名',
|
||||
|
@ -41,17 +47,60 @@ export default function () {
|
|||
{
|
||||
label: 'Base Url',
|
||||
field: 'baseUrl',
|
||||
},
|
||||
}, {label: '操作', field: 'action', width: 150}
|
||||
];
|
||||
const tableData = ref([])
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await ModelApi.getModelList({ provider: formData.value.provider, type: ModelTypeEnum.EMBEDDING });
|
||||
tableData.value = res;
|
||||
} catch (error) {
|
||||
console.error('Failed to load embedding models:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听供应商变化
|
||||
watch(() => formData.value.provider, () => {
|
||||
loadData();
|
||||
}, { immediate: true });
|
||||
|
||||
const open = () => {
|
||||
editRef.value.show({provider: formData.value.provider});
|
||||
editRef.value.show({provider: formData.value.provider, type: ModelTypeEnum.EMBEDDING});
|
||||
}
|
||||
function handleEdit(record: any) {
|
||||
editRef.value.show(record);
|
||||
}
|
||||
|
||||
async function reloadTable() {
|
||||
await loadData();
|
||||
}
|
||||
|
||||
async function handleDel(record: any) {
|
||||
dialog.confirm('确定要删除该模型吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
await ModelApi.deleteModel(record.id);
|
||||
await reloadTable();
|
||||
message.success('模型删除成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete model:', error);
|
||||
message.error('删除模型失败');
|
||||
}
|
||||
}).catch(() => {
|
||||
});
|
||||
}
|
||||
return {
|
||||
baseColumns,
|
||||
tableData,
|
||||
formData,
|
||||
editRef,
|
||||
open
|
||||
open,
|
||||
loadData,
|
||||
handleDel,
|
||||
handleEdit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import {Form} from '@/components/Form'
|
||||
import {ref} from 'vue'
|
||||
import {nextTick, ref} from 'vue'
|
||||
import {FormSchema} from "@/types/form";
|
||||
import {getSchemas} from "@/views/ai/model/embedding/composables/schemas";
|
||||
import {ModelApi} from "@/api/new-ai/model";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const emit = defineEmits(['reload'])
|
||||
const visible = ref(false)
|
||||
const formData = ref({})
|
||||
const formRef = ref()
|
||||
const schemas = ref([])
|
||||
const isEdit = ref(false)
|
||||
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
formRef.value.clearForm()
|
||||
formData.value = {}
|
||||
schemas.value = []
|
||||
nextTick(() => {
|
||||
formRef.value?.clearForm()
|
||||
formRef.value?.setSchema(schemas.value)
|
||||
})
|
||||
}
|
||||
const show = async (data: object) => {
|
||||
|
||||
const show = async (data: any = {}) => {
|
||||
visible.value = true
|
||||
isEdit.value = !!data.id
|
||||
await nextTick()
|
||||
formRef.value.setValues(data)
|
||||
schemas.value = (getSchemas(data.provider) as FormSchema[]).splice(1);
|
||||
schemas.value = getSchemas(data.provider).slice(1);
|
||||
formRef.value.setSchema(schemas.value)
|
||||
// models.value = getModels(data.provider, LLMProviders)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const form = formRef.value.getElFormRef()
|
||||
await form.validate()
|
||||
const values = formRef.value.formModel
|
||||
|
||||
if (isEdit.value) {
|
||||
await ModelApi.updateModel(values);
|
||||
ElMessage.success('更新向量模型成功');
|
||||
} else {
|
||||
await ModelApi.createModel(values);
|
||||
ElMessage.success('创建向量模型成功');
|
||||
}
|
||||
close();
|
||||
emit('reload');
|
||||
} catch (error) {
|
||||
console.error('Failed to save model:', error);
|
||||
ElMessage.error(isEdit.value ? '更新向量模型失败' : '创建向量模型失败');
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
close
|
||||
|
@ -27,15 +61,14 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="visible" draggable title="编辑" width="800px" @close="close">
|
||||
<el-dialog v-model="visible" draggable :title="isEdit ? '编辑向量模型' : '新增向量模型'" width="800px" @close="close">
|
||||
<Form ref="formRef" :model="formData" :schema="schemas" />
|
||||
<template #footer>
|
||||
<el-button type="primary">确认</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确认</el-button>
|
||||
<el-button @click="close">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import {Plus, Delete, Edit} from "@element-plus/icons-vue";
|
||||
import usePage from './composables/index'
|
||||
import Edit from "@/views/ai/model/embedding/edit.vue";
|
||||
import EditCom from "@/views/ai/model/embedding/edit.vue";
|
||||
import {LLMProviders} from "@/views/ai/model/embedding/composables/consts";
|
||||
import {Table} from "@/components/Table";
|
||||
const { baseColumns: columns, tableData, formData, editRef, open} = usePage()
|
||||
|
||||
const { baseColumns: columns, tableData, formData, editRef, open, loadData, handleEdit, handleDel} = usePage()
|
||||
|
||||
const handleReload = () => {
|
||||
loadData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -27,10 +31,15 @@ const { baseColumns: columns, tableData, formData, editRef, open} = usePage()
|
|||
show-icon
|
||||
/>
|
||||
<el-button class="my-10px" type="primary" :icon="Plus" @click="open">新增向量模型</el-button>
|
||||
<Table class="table-wrapper" height="100%" border :columns="columns" :data="tableData.concat(tableData)" :pagination="false"/>
|
||||
<Table class="table-wrapper" height="100%" border :columns="columns" :data="tableData" :pagination="false">
|
||||
<template #action="{row}">
|
||||
<el-button :icon="Edit" text @click="handleEdit(row)"/>
|
||||
<el-button :icon="Delete" text type="danger" @click="handleDel(row)"/>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<Edit ref="editRef" />
|
||||
<EditCom ref="editRef" @reload="handleReload" />
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import {computed, nextTick, ref, watch} from "vue";
|
||||
import {ProviderEnum} from "@/views/ai/model/image/composables/consts";
|
||||
import {ModelApi} from "@/api/new-ai/model";
|
||||
import { ModelTypeEnum } from "../../chatModel/composables/consts";
|
||||
import {ElTag, ElMessageBox as dialog, ElMessage as message} from "element-plus";
|
||||
|
||||
export default function () {
|
||||
|
||||
const editRef = ref()
|
||||
const formData = ref({
|
||||
provider: ProviderEnum.OPENAI,
|
||||
type: ModelTypeEnum.TEXT_IMAGE
|
||||
});
|
||||
const tableData = ref([])
|
||||
|
||||
|
@ -44,8 +48,8 @@ export default function () {
|
|||
];
|
||||
|
||||
const zhipuColumns = [...baseColumns];
|
||||
|
||||
function getColumns(provider: string) {
|
||||
console.log(provider);
|
||||
switch (provider) {
|
||||
case ProviderEnum.OPENAI: {
|
||||
return openaiColumns;
|
||||
|
@ -56,21 +60,68 @@ export default function () {
|
|||
case ProviderEnum.ZHIPU: {
|
||||
return zhipuColumns;
|
||||
}
|
||||
default: {
|
||||
return baseColumns;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const columns = computed(() => {
|
||||
nextTick();
|
||||
return getColumns(formData.value.provider)
|
||||
return [...getColumns(formData.value.provider), {label: '操作', field: 'action', width: 150}]
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await ModelApi.getModelList({ provider: formData.value.provider, type: ModelTypeEnum.TEXT_IMAGE });
|
||||
tableData.value = res;
|
||||
} catch (error) {
|
||||
console.error('Failed to load image models:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听供应商变化
|
||||
watch(() => formData.value.provider, () => {
|
||||
loadData();
|
||||
}, { immediate: true });
|
||||
|
||||
const open = () => {
|
||||
editRef.value.show({provider: formData.value.provider});
|
||||
editRef.value.show({provider: formData.value.provider, type: ModelTypeEnum.TEXT_IMAGE});
|
||||
}
|
||||
function handleEdit(record: any) {
|
||||
editRef.value.show(record);
|
||||
}
|
||||
|
||||
async function reloadTable() {
|
||||
await loadData();
|
||||
}
|
||||
|
||||
async function handleDel(record: any) {
|
||||
dialog.confirm('确定要删除该模型吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
await ModelApi.deleteModel(record.id);
|
||||
await reloadTable();
|
||||
message.success('模型删除成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete model:', error);
|
||||
message.error('删除模型失败');
|
||||
}
|
||||
}).catch(() => {
|
||||
});
|
||||
}
|
||||
return {
|
||||
columns,
|
||||
tableData,
|
||||
formData,
|
||||
editRef,
|
||||
open
|
||||
open,
|
||||
loadData,
|
||||
handleDel,
|
||||
handleEdit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,55 @@
|
|||
<script setup lang="ts">
|
||||
import {Form} from '@/components/Form'
|
||||
import {ref} from 'vue'
|
||||
import {nextTick, ref} from 'vue'
|
||||
import {FormSchema} from "@/types/form";
|
||||
import {getSchemas} from "@/views/ai/model/image/composables/schemas";
|
||||
import {ModelApi} from "@/api/new-ai/model";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const emit = defineEmits(['reload'])
|
||||
const visible = ref(false)
|
||||
const formData = ref({})
|
||||
const formRef = ref()
|
||||
const schemas = ref([])
|
||||
const isEdit = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const close = () => {
|
||||
formData.value = {}
|
||||
visible.value = false
|
||||
formRef.value.clearForm()
|
||||
schemas.value = []
|
||||
nextTick(() => {
|
||||
formRef.value?.clearForm()
|
||||
formRef.value?.setSchema(schemas.value)
|
||||
})
|
||||
}
|
||||
const show = async (data: object) => {
|
||||
|
||||
const show = async (data: any = {}) => {
|
||||
visible.value = true
|
||||
isEdit.value = !!data.id
|
||||
await nextTick()
|
||||
formRef.value.setValues(data)
|
||||
schemas.value = (getSchemas(data.provider) as FormSchema[]);
|
||||
// formRef.value.setSchema(schemas.value)
|
||||
// models.value = getModels(data.provider, LLMProviders)
|
||||
schemas.value = getSchemas(data.provider).slice(1);
|
||||
formRef.value.setSchema(schemas.value)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
|
||||
const form = formRef.value.getElFormRef()
|
||||
await form.validate()
|
||||
const values = formRef.value.formModel
|
||||
loading.value = true
|
||||
const api = isEdit.value ? ModelApi.updateModel : ModelApi.createModel
|
||||
await api(values).finally(() => loading.value = false)
|
||||
ElMessage.success(isEdit.value ? '更新模型成功' : '创建模型成功');
|
||||
close();
|
||||
emit('reload');
|
||||
} catch (error) {
|
||||
console.error('Failed to save model:', error);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
close
|
||||
|
@ -27,15 +57,13 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="visible" draggable title="编辑" width="800px" @close="close">
|
||||
<el-dialog :close-on-click-modal="false" :close-on-press-escape="false" v-model="visible" draggable :title="isEdit ? '编辑模型' : '新增模型'" width="800px" @close="close">
|
||||
<Form ref="formRef" :model="formData" :schema="schemas"/>
|
||||
<template #footer>
|
||||
<el-button type="primary">确认</el-button>
|
||||
<el-button @click="close">取消</el-button>
|
||||
<el-button :loading="loading" type="primary" @click="handleSubmit">确认</el-button>
|
||||
<el-button :loading="loading" @click="close">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import {Delete, Plus, Edit } from "@element-plus/icons-vue";
|
||||
import usePage from './composables/index'
|
||||
import {LLMProviders} from "@/views/ai/model/image/composables/consts";
|
||||
import Edit from './edit.vue'
|
||||
import EditCom from './edit.vue'
|
||||
import {Table} from "@/components/Table";
|
||||
|
||||
const {columns, tableData, formData, editRef, open} = usePage()
|
||||
const {columns, tableData, formData, editRef, open, loadData, handleEdit, handleDel} = usePage()
|
||||
|
||||
const handleReload = () => {
|
||||
loadData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -26,13 +31,19 @@ const {columns, tableData, formData, editRef, open} = usePage()
|
|||
title="鉴于很多模型的文生图效果很差甚至没有,这里只建议使用OpenAI的DALL-E模型"
|
||||
type="info"
|
||||
/>
|
||||
<el-button :icon="Plus" class="my-10px" type="primary" @click="open">新增向量模型
|
||||
<el-button :icon="Plus" class="my-10px" type="primary" @click="open">新增图像模型
|
||||
</el-button>
|
||||
<Table :columns="columns" :data="tableData.concat(tableData)" :pagination="false" border
|
||||
class="table-wrapper" height="100%"/>
|
||||
<Table
|
||||
:columns="columns" :data="tableData" :pagination="false" border
|
||||
class="table-wrapper" height="100%">
|
||||
<template #action="{row}">
|
||||
<el-button :icon="Edit" text @click="handleEdit(row)"/>
|
||||
<el-button :icon="Delete" text type="danger" @click="handleDel(row)"/>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
<Edit ref="editRef"/>
|
||||
<EditCom ref="editRef" @reload="handleReload"/>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
|
@ -51,21 +62,18 @@ const {columns, tableData, formData, editRef, open} = usePage()
|
|||
}
|
||||
|
||||
.menu {
|
||||
transition: all .15s;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
padding: 12px 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.active {
|
||||
color: #ffffff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(&.active) {
|
||||
background-color: var(--el-color-info-light-7);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import {useTable} from "@/hooks/web/useTable";
|
||||
import {FormSchema} from "@/types/form";
|
||||
import {ref} from 'vue'
|
||||
import {SliceApi} from '@/api/new-ai/slice'
|
||||
import {ElMessage} from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
knowledgeData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const tableRef = ref()
|
||||
const elTableRef = ref()
|
||||
const columns = [
|
||||
|
@ -39,16 +50,36 @@ const columns = [
|
|||
width: 200,
|
||||
}
|
||||
]
|
||||
const {tableObject, register, tableMethods} = useTable()
|
||||
const {tableObject, register, tableMethods} = useTable({
|
||||
getListApi: async (params) => {
|
||||
const res = await SliceApi.getSlicePage({
|
||||
...params,
|
||||
knowledgeId: props.knowledgeData.id
|
||||
})
|
||||
return {
|
||||
list: res.data.list || [],
|
||||
total: res.data.total || 0
|
||||
}
|
||||
},
|
||||
delListApi: async (ids) => {
|
||||
await Promise.all(ids.map(id => SliceApi.deleteSlice(id)))
|
||||
ElMessage.success('删除成功')
|
||||
},
|
||||
response: {
|
||||
list: 'list',
|
||||
total: 'total'
|
||||
}
|
||||
})
|
||||
const schema = ref<FormSchema[]>([
|
||||
{
|
||||
label: '所属文档',
|
||||
field: 'text',
|
||||
field: 'docId',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '150px'
|
||||
}
|
||||
},
|
||||
placeholder: '请选择所属文档'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
@ -60,17 +91,29 @@ const pagination = computed(() => {
|
|||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
register(tableRef.value, elTableRef.value)
|
||||
register(tableRef,elTableRef,)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<content-wrap>
|
||||
<Search :schema="schema"/>
|
||||
</content-wrap>
|
||||
<content-wrap>
|
||||
<Table ref="tableRef" :pagination="pagination" :columns="columns"/>
|
||||
</content-wrap>
|
||||
<div>
|
||||
<Table
|
||||
ref="tableRef"
|
||||
v-model:table="tableObject"
|
||||
:columns="columns"
|
||||
:schema="schema"
|
||||
:label-width="100"
|
||||
:search-table-height="true">
|
||||
<template #action="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="tableMethods.delList([row.id], false)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
import {useTable} from "@/hooks/web/useTable";
|
||||
import {FormSchema} from "@/types/form";
|
||||
import {ref} from 'vue'
|
||||
import {DocsApi} from '@/api/new-ai/docs'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
knowledgeData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const tableRef = ref()
|
||||
const elTableRef = ref()
|
||||
|
||||
const columns = [
|
||||
{
|
||||
label: '文档名称',
|
||||
|
@ -45,40 +57,86 @@ const columns = [
|
|||
width: 200,
|
||||
}
|
||||
]
|
||||
const {tableObject, register, tableMethods} = useTable()
|
||||
|
||||
const {tableObject, register, tableMethods} = useTable({
|
||||
getListApi: async (params) => {
|
||||
const res = await DocsApi.getDocsPage({
|
||||
...params,
|
||||
knowledgeId: props.knowledgeData.id
|
||||
})
|
||||
return {
|
||||
list: res.data.list || [],
|
||||
total: res.data.total || 0
|
||||
}
|
||||
},
|
||||
delListApi: async (ids) => {
|
||||
await Promise.all(ids.map(id => DocsApi.deleteDocs(id)))
|
||||
ElMessage.success('删除成功')
|
||||
},
|
||||
response: {
|
||||
list: 'list',
|
||||
total: 'total'
|
||||
}
|
||||
})
|
||||
|
||||
const schema = ref<FormSchema[]>([
|
||||
{
|
||||
label: '文档名称',
|
||||
field: 'text',
|
||||
component: 'Select',
|
||||
field: 'name',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
style: {
|
||||
width: '150px'
|
||||
}
|
||||
},
|
||||
placeholder: '请输入文档名称'
|
||||
}
|
||||
}
|
||||
])
|
||||
const pagination = computed(() => {
|
||||
return {
|
||||
pageSize: tableObject.pageSize,
|
||||
currentPage: tableObject.currentPage,
|
||||
total: tableObject.total,
|
||||
|
||||
// 重新向量化
|
||||
const handleReEmbed = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认要重新向量化该文档吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await DocsApi.reEmbedDocs(row.id)
|
||||
ElMessage.success('重新向量化成功')
|
||||
tableMethods.getList()
|
||||
} catch (error) {
|
||||
console.error('Failed to re-embed:', error)
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
register(tableRef.value, elTableRef.value)
|
||||
})
|
||||
}
|
||||
|
||||
// 注册表格
|
||||
register(tableRef as any ,elTableRef as any)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<content-wrap>
|
||||
<Search :schema="schema"/>
|
||||
</content-wrap>
|
||||
<content-wrap>
|
||||
<Table ref="tableRef" :pagination="pagination" :columns="columns"/>
|
||||
</content-wrap>
|
||||
<div>
|
||||
<Table
|
||||
ref="tableRef"
|
||||
v-model:table="tableObject"
|
||||
:columns="columns"
|
||||
:schema="schema"
|
||||
:label-width="100"
|
||||
:search-table-height="true">
|
||||
<template #action="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleReEmbed(row)">
|
||||
重新向量化
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="tableMethods.delList([row.id])">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
<script lang="ts" setup>
|
||||
import {ref, onMounted} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {CopyDocument, UploadFilled, Files, Document, Search} from '@element-plus/icons-vue'
|
||||
import DataCut from "@/views/knowledge/dataset-form/components/data-cut.vue";
|
||||
import DataImport from "@/views/knowledge/dataset-form/components/data-import.vue";
|
||||
import DataDocument from "@/views/knowledge/dataset-form/components/data-document.vue";
|
||||
import DataEmbedding from "@/views/knowledge/dataset-form/components/data-embedding.vue";
|
||||
const Route = useRoute()
|
||||
const Router = useRouter()
|
||||
import {KnowledgeApi} from '@/api/new-ai/knowledge'
|
||||
import {ElMessage} from 'element-plus'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const active = ref('1')
|
||||
const knowledgeData = ref({})
|
||||
|
||||
const tabs = ref([
|
||||
{
|
||||
label: '数据导入',
|
||||
|
@ -34,6 +40,23 @@ const tabs = ref([
|
|||
component: DataEmbedding
|
||||
}
|
||||
])
|
||||
|
||||
const loadKnowledge = async () => {
|
||||
const id = route.query.id as string
|
||||
if (id) {
|
||||
try {
|
||||
const res = await KnowledgeApi.getKnowledge(id)
|
||||
knowledgeData.value = res.data
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge:', error)
|
||||
ElMessage.error('加载知识库失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadKnowledge()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -41,7 +64,7 @@ const tabs = ref([
|
|||
<div class="flex flex-col mr-30px w-350px">
|
||||
<el-button
|
||||
class="w-full mb-10px" plain type="primary"
|
||||
@click="Router.push({ path: '/ai/console/knowledge' })">知识库列表
|
||||
@click="router.push({ path: '/ai/console/knowledge' })">知识库列表
|
||||
</el-button>
|
||||
<el-scrollbar class="flex-1">
|
||||
<div class="py-20px flex items-center border-b-solid border-gray border-1 mb-10px">
|
||||
|
@ -86,16 +109,22 @@ const tabs = ref([
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped>
|
||||
.children {
|
||||
height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - (var(--app-content-padding) * 3)) !important;
|
||||
box-sizing: border-box;
|
||||
|
||||
& > div {
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.icon-bg {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
padding: 15px !important;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="knowledge-base-container">
|
||||
<div class="card-container">
|
||||
<el-card class="create-card" shadow="hover">
|
||||
<el-card class="create-card" shadow="hover" @click="toDatasetForm">
|
||||
<div class="create-content">
|
||||
<el-icon class="create-icon"><Plus /></el-icon>
|
||||
<span class="create-text">创建知识库</span>
|
||||
|
@ -11,21 +11,21 @@
|
|||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card v-for="index in 4" :key="index" class="document-card" shadow="hover"
|
||||
@click="toDataset(index)">
|
||||
<el-card v-for="item in tableData" :key="item.id" class="document-card" shadow="hover"
|
||||
@click="toDataset(item.id)">
|
||||
<div class="document-header">
|
||||
<el-icon>
|
||||
<Folder/>
|
||||
</el-icon>
|
||||
<span>接口鉴权示例代码.md</span>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="document-info">
|
||||
<el-tag size="small">1 文档</el-tag>
|
||||
<el-tag size="small" type="info">5 千字符</el-tag>
|
||||
<el-tag size="small" type="warning">0 关联应用</el-tag>
|
||||
<el-tag size="small">{{ item.documentCount || 0 }} 文档</el-tag>
|
||||
<el-tag size="small" type="info">{{ item.characterCount || 0 }} 千字符</el-tag>
|
||||
<el-tag size="small" type="warning">{{ item.appCount || 0 }} 关联应用</el-tag>
|
||||
</div>
|
||||
<p class="document-description">
|
||||
useful for when you want to answer queries about the 接口鉴权示例代码.md
|
||||
{{ item.description || '暂无描述' }}
|
||||
</p>
|
||||
</el-card>
|
||||
</div>
|
||||
|
@ -47,94 +47,128 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue'
|
||||
import {Folder, Plus} from '@element-plus/icons-vue'
|
||||
import {useRouter} from "vue-router";
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted} from 'vue'
|
||||
import {Plus, Folder} from '@element-plus/icons-vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {KnowledgeApi} from '@/api/new-ai/knowledge'
|
||||
|
||||
const router = useRouter()
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(100) // 假设总共有100条数据
|
||||
const total = ref(0)
|
||||
const tableData = ref([])
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
console.log(`每页 ${val} 条`)
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await KnowledgeApi.getKnowledgePage({
|
||||
pageNo: currentPage.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
tableData.value = res.data.list || []
|
||||
total.value = res.data.total || 0
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge list:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
console.log(`当前页: ${val}`)
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageSize.value = val
|
||||
loadData()
|
||||
}
|
||||
const toDataset = (index) => {
|
||||
router.push({path: '/ai/console/knowledge/' + index})
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val
|
||||
loadData()
|
||||
}
|
||||
|
||||
const toDataset = (id: string) => {
|
||||
router.push(`/ai/console/knowledge/${id}`)
|
||||
}
|
||||
|
||||
const toDatasetForm = () => {
|
||||
router.push('/ai/console/knowledge/1')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.knowledge-base-container {
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
position: absolute;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
bottom: 40px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* Enable wrapping */
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: auto; /* Pushes pagination to the bottom */
|
||||
}
|
||||
|
||||
.create-card, .document-card {
|
||||
flex: 1 1 360px; /* Allow cards to grow and shrink */
|
||||
min-width: 0;
|
||||
max-width: 400px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.create-card {
|
||||
background-color: rgba(168, 168, 168, 0.22);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.create-card:hover {
|
||||
background-color: #fff;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.create-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.create-icon {
|
||||
font-size: 24px;
|
||||
color: #409EFF;
|
||||
font-size: 40px;
|
||||
color: var(--el-color-primary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.create-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.create-footer {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
line-height: 1.5;
|
||||
padding: 10px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.document-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.document-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.document-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.document-header .el-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 20px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.document-header span {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.document-info {
|
||||
|
@ -144,15 +178,16 @@ const toDataset = (index) => {
|
|||
}
|
||||
|
||||
.document-description {
|
||||
color: #606266;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
|
|
Loading…
Reference in New Issue