Compare commits
18 Commits
master
...
dev-featur
Author | SHA1 | Date |
---|---|---|
|
aaa0967f24 | |
|
1285218eea | |
|
373d8b771b | |
|
c117e5f59b | |
|
ac2d981a23 | |
|
1bc33d2784 | |
|
cf1c7210f7 | |
|
46d23bd6b6 | |
|
a49de52587 | |
|
05609d63c2 | |
|
6327866533 | |
|
25383d56c6 | |
|
2fd76aa5b9 | |
|
eeb8d31f99 | |
|
a92e21767b | |
|
cbc3f3296a | |
|
eb5af0eec6 | |
|
f62e8d4cea |
|
@ -62,7 +62,7 @@
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
},
|
},
|
||||||
"[typescriptreact]": {
|
"[typescriptreact]": {
|
||||||
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||||
|
|
|
@ -29,7 +29,9 @@
|
||||||
"@form-create/designer": "^3.2.6",
|
"@form-create/designer": "^3.2.6",
|
||||||
"@form-create/element-ui": "^3.2.11",
|
"@form-create/element-ui": "^3.2.11",
|
||||||
"@iconify/iconify": "^3.1.1",
|
"@iconify/iconify": "^3.1.1",
|
||||||
|
"@iconify/vue": "^4.3.0",
|
||||||
"@microsoft/fetch-event-source": "^2.0.1",
|
"@microsoft/fetch-event-source": "^2.0.1",
|
||||||
|
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||||
"@videojs-player/vue": "^1.0.0",
|
"@videojs-player/vue": "^1.0.0",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
@ -53,6 +55,7 @@
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
|
"markdown-it-link-attributes": "^4.0.1",
|
||||||
"markmap-common": "^0.16.0",
|
"markmap-common": "^0.16.0",
|
||||||
"markmap-lib": "^0.16.1",
|
"markmap-lib": "^0.16.1",
|
||||||
"markmap-toolbar": "^0.17.0",
|
"markmap-toolbar": "^0.17.0",
|
||||||
|
@ -67,6 +70,7 @@
|
||||||
"sortablejs": "^1.15.3",
|
"sortablejs": "^1.15.3",
|
||||||
"steady-xml": "^0.1.0",
|
"steady-xml": "^0.1.0",
|
||||||
"url": "^0.11.3",
|
"url": "^0.11.3",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"video.js": "^7.21.5",
|
"video.js": "^7.21.5",
|
||||||
"vue": "3.5.12",
|
"vue": "3.5.12",
|
||||||
"vue-dompurify-html": "^4.1.4",
|
"vue-dompurify-html": "^4.1.4",
|
||||||
|
@ -88,6 +92,7 @@
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/qs": "^6.9.12",
|
"@types/qs": "^6.9.12",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||||
"@typescript-eslint/parser": "^7.1.0",
|
"@typescript-eslint/parser": "^7.1.0",
|
||||||
"@unocss/eslint-config": "^0.57.4",
|
"@unocss/eslint-config": "^0.57.4",
|
||||||
|
|
346
pnpm-lock.yaml
346
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,20 @@
|
||||||
|
export enum ModelTypeEnum {
|
||||||
|
CHAT = 'CHAT',
|
||||||
|
EMBEDDING = 'EMBEDDING',
|
||||||
|
TEXT_IMAGE = 'TEXT_IMAGE',
|
||||||
|
WEB_SEARCH = 'WEB_SEARCH',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏API密钥中间部分
|
||||||
|
* @param key API密钥
|
||||||
|
* @returns 隐藏处理后的密钥
|
||||||
|
*/
|
||||||
|
export function hideKey(key: string): string {
|
||||||
|
if (!key) return ''
|
||||||
|
const length = key.length
|
||||||
|
if (length <= 8) return key
|
||||||
|
const start = key.slice(0, 4)
|
||||||
|
const end = key.slice(-4)
|
||||||
|
return `${start}****${end}`
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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 应用 API
|
||||||
|
export const AppApi = {
|
||||||
|
// 获取应用列表
|
||||||
|
getAppList: async (params: any) => {
|
||||||
|
return await request.get({ url: '/chat/aigc/app/list', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取应用分页数据
|
||||||
|
getAppPage: async (params: any) => {
|
||||||
|
return await request.get({ url: '/chat/aigc/app/page', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取应用详情
|
||||||
|
getApp: async (id: string) => {
|
||||||
|
return await request.get({ url: `/chat/aigc/app/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 根据模型ID获取应用
|
||||||
|
getAppByModelId: async (id: string) => {
|
||||||
|
return await request.get({ url: `/chat/aigc/app/byModelId/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取应用API通道
|
||||||
|
getAppApiChannel: async (appId: string) => {
|
||||||
|
return await request.get({ url: `/chat/aigc/app/channel/api/${appId}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增应用
|
||||||
|
createApp: async (data: any) => {
|
||||||
|
return await request.post({ url: '/chat/aigc/app', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新应用
|
||||||
|
updateApp: async (data: any) => {
|
||||||
|
return await request.put({ url: '/chat/aigc/app', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除应用
|
||||||
|
deleteApp: async (id: string) => {
|
||||||
|
return await request.delete({ url: `/chat/aigc/app/${id}` })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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 应用管理 API
|
||||||
|
export const AppApiManagement = {
|
||||||
|
// 获取API列表
|
||||||
|
getApiList: async (params: any) => {
|
||||||
|
return await request.get({ url: '/chat/aigc/app/api/list', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取API分页数据
|
||||||
|
getApiPage: async (params: any) => {
|
||||||
|
return await request.get({ url: '/chat/aigc/app/api/page', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取API详情
|
||||||
|
getApi: async (id: string) => {
|
||||||
|
return await request.get({ url: `/chat/aigc/app/api/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增API
|
||||||
|
createApi: async (data: any) => {
|
||||||
|
return await request.post({ url: '/chat/aigc/app/api', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新API
|
||||||
|
updateApi: async (data: any) => {
|
||||||
|
return await request.put({ url: '/chat/aigc/app/api', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除API
|
||||||
|
deleteApi: async (id: string) => {
|
||||||
|
return await request.delete({ url: `/chat/aigc/app/api/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 发布API
|
||||||
|
publishApi: async (id: string) => {
|
||||||
|
return await request.put({ url: `/chat/aigc/app/api/publish/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 下线API
|
||||||
|
offlineApi: async (id: string) => {
|
||||||
|
return await request.put({ url: `/chat/aigc/app/api/offline/${id}` })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* 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'
|
||||||
|
import { AxiosProgressEvent } from 'axios'
|
||||||
|
|
||||||
|
export function chat(
|
||||||
|
data: any,
|
||||||
|
controller: AbortController,
|
||||||
|
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||||
|
) {
|
||||||
|
return request.post({
|
||||||
|
url: '/chat/aigc/chat/completions',
|
||||||
|
data,
|
||||||
|
signal: controller.signal,
|
||||||
|
onDownloadProgress,
|
||||||
|
isReturnNativeResponse: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clean(conversationId: string | null) {
|
||||||
|
return request.delete({
|
||||||
|
url: `/chat/aigc/chat/messages/clean/${conversationId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMessages(conversationId?: string | number) {
|
||||||
|
return request.get({
|
||||||
|
url: `/chat/aigc/chat/messages/${conversationId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAppInfo(params: any) {
|
||||||
|
return request.get({
|
||||||
|
url: `/chat/aigc/app/info`,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImageModels() {
|
||||||
|
return request.get({
|
||||||
|
url: '/chat/aigc/chat/getImageModels'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 生成图片
|
||||||
|
*/
|
||||||
|
export function genImage(data: any) {
|
||||||
|
return request.post({
|
||||||
|
url: '/chat/aigc/chat/image',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 生成思维导图
|
||||||
|
*/
|
||||||
|
export function genMindMap(data: any) {
|
||||||
|
return request.post({
|
||||||
|
url: '/chat/aigc/chat/mindmap',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
export function page(params: any) {
|
||||||
|
return request.get({
|
||||||
|
url: '/chat/aigc/conversation/page',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function del(id: string) {
|
||||||
|
return request.delete({
|
||||||
|
url: `/chat/aigc/conversation/${id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMessages(conversationId: string) {
|
||||||
|
return request.get({
|
||||||
|
url: `/chat/aigc/conversation/messages/${conversationId}`,
|
||||||
|
});
|
||||||
|
}
|
|
@ -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: '/chat/aigc/docs/page', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获得文档列表
|
||||||
|
async getDocsList(params: any) {
|
||||||
|
return await request.get({ url: '/chat/aigc/docs/list', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获得文档详情
|
||||||
|
async getDocsById(id: string) {
|
||||||
|
return await request.get({ url: `/chat/aigc/docs/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建文档
|
||||||
|
async addDocs(data: any) {
|
||||||
|
return await request.post({ url: '/chat/aigc/docs', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新文档
|
||||||
|
async updateDocs(data: any) {
|
||||||
|
return await request.put({ url: '/chat/aigc/docs', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除文档
|
||||||
|
async deleteDocs(id: string) {
|
||||||
|
return await request.delete({ url: `/chat/aigc/docs/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重新向量化
|
||||||
|
async reEmbedDocs(id: string) {
|
||||||
|
return await request.get({ url: `/chat/aigc/embedding/re-embed/${id}` })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
|
||||||
|
|
||||||
|
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: '/chat/aigc/embed-store/list', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获得嵌入存储分页
|
||||||
|
getEmbedStorePage: async (params: any) => {
|
||||||
|
return await request.get({ url: '/chat/aigc/embed-store/page', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获得嵌入存储详情
|
||||||
|
getEmbedStore: async (id: string) => {
|
||||||
|
return await request.get({ url: `/chat/aigc/embed-store/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建嵌入存储
|
||||||
|
createEmbedStore: async (data: any) => {
|
||||||
|
return await request.post({ url: '/chat/aigc/embed-store', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新嵌入存储
|
||||||
|
updateEmbedStore: async (data: any) => {
|
||||||
|
return await request.put({ url: '/chat/aigc/embed-store', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除嵌入存储
|
||||||
|
deleteEmbedStore: async (id: string) => {
|
||||||
|
return await request.delete({ url: `/chat/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: '/chat/aigc/embedding/text', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 嵌入搜索
|
||||||
|
embeddingSearch: async (data: any) => {
|
||||||
|
return await request.post({ url: '/chat/aigc/embedding/search', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 文档嵌入
|
||||||
|
embeddingDocs: async (
|
||||||
|
knowledgeId: string,
|
||||||
|
data: any,
|
||||||
|
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
|
||||||
|
) => {
|
||||||
|
return await request.post({
|
||||||
|
url: `/chat/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: '/chat/aigc/knowledge/list', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获得知识库分页
|
||||||
|
getKnowledgePage: async (params: any) => {
|
||||||
|
return await request.get({ url: '/chat/aigc/knowledge/page', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获得知识库详情
|
||||||
|
getKnowledge: async (id: string) => {
|
||||||
|
return await request.get({ url: `/chat/aigc/knowledge/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建知识库
|
||||||
|
createKnowledge: async (data: any) => {
|
||||||
|
return await request.post({ url: '/chat/aigc/knowledge', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新知识库
|
||||||
|
updateKnowledge: async (data: any) => {
|
||||||
|
return await request.put({ url: '/chat/aigc/knowledge', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除知识库
|
||||||
|
deleteKnowledge: async (id: string) => {
|
||||||
|
return await request.delete({ url: `/chat/aigc/knowledge/${id}` })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
|
||||||
|
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息列表接口参数
|
||||||
|
*/
|
||||||
|
export interface MessageParams {
|
||||||
|
text?: string;
|
||||||
|
username?: string;
|
||||||
|
role?: string;
|
||||||
|
pageNum?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息列表
|
||||||
|
*/
|
||||||
|
export function list(params: MessageParams) {
|
||||||
|
return request.get({
|
||||||
|
url: '/chat/aigc/message/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取消息列表
|
||||||
|
*/
|
||||||
|
export function page(params: MessageParams) {
|
||||||
|
return request.get({
|
||||||
|
url: '/chat/aigc/message/page',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加消息
|
||||||
|
*/
|
||||||
|
export function add(data: any) {
|
||||||
|
return request.post({
|
||||||
|
url: '/chat/aigc/message',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新消息
|
||||||
|
*/
|
||||||
|
export function update(data: any) {
|
||||||
|
return request.put({
|
||||||
|
url: '/chat/aigc/message',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息
|
||||||
|
*/
|
||||||
|
export function del(id: string) {
|
||||||
|
return request.delete({
|
||||||
|
url: `/chat/aigc/message/${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,71 @@
|
||||||
|
/*
|
||||||
|
* 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'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OSS对象存储服务接口
|
||||||
|
*/
|
||||||
|
export class OssApi {
|
||||||
|
/**
|
||||||
|
* 获取OSS上传策略
|
||||||
|
*/
|
||||||
|
static policy() {
|
||||||
|
return request.get({
|
||||||
|
url: '/chat/oss/policy'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件列表
|
||||||
|
*/
|
||||||
|
static list(params: any) {
|
||||||
|
return request.get({
|
||||||
|
url: '/chat/oss/list',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
*/
|
||||||
|
static del(objectName: string) {
|
||||||
|
return request.delete({
|
||||||
|
url: `/chat/oss/${objectName}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件
|
||||||
|
*/
|
||||||
|
static uploadUrl = '/chat/aigc/oss/upload'
|
||||||
|
|
||||||
|
static upload(data: any) {
|
||||||
|
return request.upload({
|
||||||
|
url: '/chat/aigc/oss/upload',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件访问URL
|
||||||
|
*/
|
||||||
|
static getUrl(objectName: string) {
|
||||||
|
return request.get({
|
||||||
|
url: `/chat/oss/url/${objectName}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: '/chat/aigc/docs/slice/page', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获得切片列表
|
||||||
|
getSliceList: async (params: any) => {
|
||||||
|
return await request.get({ url: '/chat/aigc/docs/slice/list', params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获得切片详情
|
||||||
|
getSlice: async (id: string) => {
|
||||||
|
return await request.get({ url: `/chat/aigc/docs/slice/${id}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 创建切片
|
||||||
|
createSlice: async (data: any) => {
|
||||||
|
return await request.post({ url: '/chat/aigc/docs/slice', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新切片
|
||||||
|
updateSlice: async (data: any) => {
|
||||||
|
return await request.put({ url: '/chat/aigc/docs/slice', data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除切片
|
||||||
|
deleteSlice: async (id: string) => {
|
||||||
|
return await request.delete({ url: `/chat/aigc/docs/slice/${id}` })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
|
||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
export function getReqChartBy30() {
|
||||||
|
return request.get({
|
||||||
|
url: `/chat/aigc/statistic/requestBy30`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReqChart() {
|
||||||
|
return request.get({
|
||||||
|
url: `/chat/aigc/statistic/request`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTokenChartBy30() {
|
||||||
|
return request.get({
|
||||||
|
url: `/chat/aigc/statistic/tokenBy30`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTokenChart() {
|
||||||
|
return request.get({
|
||||||
|
url: `/chat/aigc/statistic/token`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHomeData() {
|
||||||
|
return request.get({
|
||||||
|
url: `/chat/aigc/statistic/home`,
|
||||||
|
});
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import pageError from '@/assets/svgs/404.svg'
|
||||||
import networkError from '@/assets/svgs/500.svg'
|
import networkError from '@/assets/svgs/500.svg'
|
||||||
import noPermission from '@/assets/svgs/403.svg'
|
import noPermission from '@/assets/svgs/403.svg'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
import { onBeforeRouteUpdate } from 'vue-router'
|
||||||
defineOptions({ name: 'Error' })
|
defineOptions({ name: 'Error' })
|
||||||
|
|
||||||
interface ErrorMap {
|
interface ErrorMap {
|
||||||
|
|
|
@ -113,11 +113,16 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const setValue = (key: string, value: any) => {
|
||||||
|
formModel.value[key] = value
|
||||||
|
}
|
||||||
const getElFormRef = (): ComponentRef<typeof ElForm> => {
|
const getElFormRef = (): ComponentRef<typeof ElForm> => {
|
||||||
return unref(elFormRef) as ComponentRef<typeof ElForm>
|
return unref(elFormRef) as ComponentRef<typeof ElForm>
|
||||||
}
|
}
|
||||||
|
const clearForm = () => {
|
||||||
|
formModel.value = {}
|
||||||
|
getElFormRef().resetFields()
|
||||||
|
}
|
||||||
expose({
|
expose({
|
||||||
setValues,
|
setValues,
|
||||||
formModel,
|
formModel,
|
||||||
|
@ -125,7 +130,9 @@ export default defineComponent({
|
||||||
delSchema,
|
delSchema,
|
||||||
addSchema,
|
addSchema,
|
||||||
setSchema,
|
setSchema,
|
||||||
getElFormRef
|
getElFormRef,
|
||||||
|
clearForm,
|
||||||
|
setValue
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听表单结构化数组,重新生成formModel
|
// 监听表单结构化数组,重新生成formModel
|
||||||
|
@ -226,6 +233,7 @@ export default defineComponent({
|
||||||
return slots[item.field] ? (
|
return slots[item.field] ? (
|
||||||
getSlot(slots, item.field, formModel.value)
|
getSlot(slots, item.field, formModel.value)
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<Com
|
<Com
|
||||||
vModel={formModel.value[item.field]}
|
vModel={formModel.value[item.field]}
|
||||||
{...(autoSetPlaceholder && setTextPlaceholder(item))}
|
{...(autoSetPlaceholder && setTextPlaceholder(item))}
|
||||||
|
@ -238,6 +246,8 @@ export default defineComponent({
|
||||||
>
|
>
|
||||||
{{ ...slotsMap }}
|
{{ ...slotsMap }}
|
||||||
</Com>
|
</Com>
|
||||||
|
</>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, useAttrs } from 'vue';
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
|
|
||||||
|
const bindAttrs = computed<{ class: string; style: string }>(() => ({
|
||||||
|
class: (attrs.class as string) || '',
|
||||||
|
style: (attrs.style as string) || '',
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Icon :icon="icon!" v-bind="bindAttrs" />
|
||||||
|
</template>
|
|
@ -22,13 +22,13 @@
|
||||||
>
|
>
|
||||||
<el-button type="primary">
|
<el-button type="primary">
|
||||||
<Icon icon="ep:upload-filled" />
|
<Icon icon="ep:upload-filled" />
|
||||||
选取文件
|
{{ props.drag ? '拖拽或' : ''}}选取文件
|
||||||
</el-button>
|
</el-button>
|
||||||
<template v-if="isShowTip" #tip>
|
<template v-if="isShowTip" #tip>
|
||||||
<div style="font-size: 8px">
|
<div >
|
||||||
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 8px">
|
<div >
|
||||||
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
|
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -79,7 +79,9 @@ const props = defineProps({
|
||||||
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
|
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
|
||||||
borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
|
borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
|
||||||
showDelete: propTypes.bool.def(true), // 是否显示删除按钮
|
showDelete: propTypes.bool.def(true), // 是否显示删除按钮
|
||||||
showBtnText: propTypes.bool.def(true) // 是否显示按钮文字
|
showBtnText: propTypes.bool.def(true), // 是否显示按钮文字
|
||||||
|
customApi: propTypes.func.def(() => undefined), // 上传接口地址 ==> 非必传(默认为 '')
|
||||||
|
successBefore: propTypes.func.def(() => undefined), // 上传成功之前的钩子 ==> 非必传(默认为 undefined)
|
||||||
})
|
})
|
||||||
const { t } = useI18n() // 国际化
|
const { t } = useI18n() // 国际化
|
||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
|
@ -99,7 +101,7 @@ const deleteImg = () => {
|
||||||
emit('update:modelValue', '')
|
emit('update:modelValue', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { uploadUrl, httpRequest } = useUpload()
|
const { uploadUrl, httpRequest } = useUpload({ customApi: props.customApi })
|
||||||
|
|
||||||
const editImg = () => {
|
const editImg = () => {
|
||||||
const dom = document.querySelector(`#${uuid.value} .el-upload__input`)
|
const dom = document.querySelector(`#${uuid.value} .el-upload__input`)
|
||||||
|
@ -117,8 +119,12 @@ const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||||
|
|
||||||
// 图片上传成功提示
|
// 图片上传成功提示
|
||||||
const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
|
const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
|
||||||
|
let params = res
|
||||||
|
if (props.successBefore) {
|
||||||
|
params = props.successBefore(res)
|
||||||
|
}
|
||||||
message.success('上传成功')
|
message.success('上传成功')
|
||||||
emit('update:modelValue', res.data)
|
emit('update:modelValue', params.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图片上传错误提示
|
// 图片上传错误提示
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const getUploadUrl = (): string => {
|
||||||
return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
|
return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpload = () => {
|
export const useUpload = (defaultParams?: { customApi: any }) => {
|
||||||
// 后端上传地址
|
// 后端上传地址
|
||||||
const uploadUrl = getUploadUrl()
|
const uploadUrl = getUploadUrl()
|
||||||
// 是否使用前端直连上传
|
// 是否使用前端直连上传
|
||||||
|
@ -40,7 +40,8 @@ export const useUpload = () => {
|
||||||
// 模式二:后端上传
|
// 模式二:后端上传
|
||||||
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
|
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
FileApi.updateFile({ file: options.file })
|
const Api = defaultParams?.customApi || FileApi.updateFile
|
||||||
|
Api({ file: options.file })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
resolve(res)
|
resolve(res)
|
||||||
|
|
|
@ -149,7 +149,7 @@ service.interceptors.response.use(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (code === 500) {
|
} else if (code === 500) {
|
||||||
ElMessage.error(t('sys.api.errMsg500'))
|
ElMessage.error(msg || t('sys.api.errMsg500'))
|
||||||
return Promise.reject(new Error(msg))
|
return Promise.reject(new Error(msg))
|
||||||
} else if (code === 901) {
|
} else if (code === 901) {
|
||||||
ElMessage.error({
|
ElMessage.error({
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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 type { GlobConfig } from '/#/config';
|
||||||
|
|
||||||
|
import { warn } from '@/utils/log';
|
||||||
|
import { getAppEnvConfig } from '@/utils/env';
|
||||||
|
|
||||||
|
export const useGlobSetting = (): Readonly<GlobConfig> => {
|
||||||
|
const {
|
||||||
|
VITE_GLOB_APP_TITLE,
|
||||||
|
VITE_GLOB_API_URL,
|
||||||
|
VITE_GLOB_APP_SHORT_NAME,
|
||||||
|
VITE_GLOB_API_URL_PREFIX,
|
||||||
|
VITE_GLOB_UPLOAD_URL,
|
||||||
|
VITE_GLOB_IMG_URL,
|
||||||
|
} = getAppEnvConfig();
|
||||||
|
|
||||||
|
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
|
||||||
|
warn(
|
||||||
|
`VITE_GLOB_APP_SHORT_NAME Variables can only be characters/underscores, please modify in the environment variables and re-running.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take global configuration
|
||||||
|
const glob: Readonly<GlobConfig> = {
|
||||||
|
title: VITE_GLOB_APP_TITLE,
|
||||||
|
apiUrl: VITE_GLOB_API_URL,
|
||||||
|
shortName: VITE_GLOB_APP_SHORT_NAME,
|
||||||
|
urlPrefix: VITE_GLOB_API_URL_PREFIX,
|
||||||
|
uploadUrl: VITE_GLOB_UPLOAD_URL,
|
||||||
|
imgUrl: VITE_GLOB_IMG_URL,
|
||||||
|
};
|
||||||
|
return glob as Readonly<GlobConfig>;
|
||||||
|
};
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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 { computed } from 'vue';
|
||||||
|
import { useDesignSettingStore } from '@/store/modules/designSetting';
|
||||||
|
|
||||||
|
export function useDesignSetting() {
|
||||||
|
const designStore = useDesignSettingStore();
|
||||||
|
|
||||||
|
const getDarkTheme = computed(() => designStore.darkTheme);
|
||||||
|
|
||||||
|
const getAppTheme = computed(() => designStore.appTheme);
|
||||||
|
|
||||||
|
const getAppThemeList = computed(() => designStore.appThemeList);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getDarkTheme,
|
||||||
|
getAppTheme,
|
||||||
|
getAppThemeList,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* 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 { computed } from 'vue';
|
||||||
|
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||||
|
|
||||||
|
export function useProjectSetting() {
|
||||||
|
const projectStore = useProjectSettingStore();
|
||||||
|
|
||||||
|
const navMode = computed(() => projectStore.navMode);
|
||||||
|
|
||||||
|
const navTheme = computed(() => projectStore.navTheme);
|
||||||
|
|
||||||
|
const isMobile = computed(() => projectStore.isMobile);
|
||||||
|
|
||||||
|
const headerSetting = computed(() => projectStore.headerSetting);
|
||||||
|
|
||||||
|
const multiTabsSetting = computed(() => projectStore.multiTabsSetting);
|
||||||
|
|
||||||
|
const menuSetting = computed(() => projectStore.menuSetting);
|
||||||
|
|
||||||
|
const crumbsSetting = computed(() => projectStore.crumbsSetting);
|
||||||
|
|
||||||
|
const permissionMode = computed(() => projectStore.permissionMode);
|
||||||
|
|
||||||
|
const showFooter = computed(() => projectStore.showFooter);
|
||||||
|
|
||||||
|
const isPageAnimate = computed(() => projectStore.isPageAnimate);
|
||||||
|
|
||||||
|
const pageAnimateType = computed(() => projectStore.pageAnimateType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
navMode,
|
||||||
|
navTheme,
|
||||||
|
isMobile,
|
||||||
|
headerSetting,
|
||||||
|
multiTabsSetting,
|
||||||
|
menuSetting,
|
||||||
|
crumbsSetting,
|
||||||
|
permissionMode,
|
||||||
|
showFooter,
|
||||||
|
isPageAnimate,
|
||||||
|
pageAnimateType,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
|
||||||
|
|
||||||
|
const setting = {
|
||||||
|
//导航模式 vertical 左侧菜单模式 horizontal 顶部菜单模式
|
||||||
|
navMode: 'vertical',
|
||||||
|
//导航风格 dark 暗色侧边栏 light 白色侧边栏 header-dark 暗色顶栏
|
||||||
|
navTheme: 'dark',
|
||||||
|
// 是否处于移动端模式
|
||||||
|
isMobile: false,
|
||||||
|
//顶部
|
||||||
|
headerSetting: {
|
||||||
|
//背景色
|
||||||
|
bgColor: '#fff',
|
||||||
|
//固定顶部
|
||||||
|
fixed: true,
|
||||||
|
//显示重载按钮
|
||||||
|
isReload: true,
|
||||||
|
},
|
||||||
|
//页脚
|
||||||
|
showFooter: false,
|
||||||
|
//多标签
|
||||||
|
multiTabsSetting: {
|
||||||
|
//背景色
|
||||||
|
bgColor: '#fff',
|
||||||
|
//是否显示
|
||||||
|
show: true,
|
||||||
|
//固定多标签
|
||||||
|
fixed: true,
|
||||||
|
},
|
||||||
|
//菜单
|
||||||
|
menuSetting: {
|
||||||
|
//最小宽度
|
||||||
|
minMenuWidth: 64,
|
||||||
|
//菜单宽度
|
||||||
|
menuWidth: 200,
|
||||||
|
//固定菜单
|
||||||
|
fixed: true,
|
||||||
|
//分割菜单
|
||||||
|
mixMenu: false,
|
||||||
|
//触发移动端侧边栏的宽度
|
||||||
|
mobileWidth: 800,
|
||||||
|
// 折叠菜单
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
//面包屑
|
||||||
|
crumbsSetting: {
|
||||||
|
//是否显示
|
||||||
|
show: false,
|
||||||
|
//显示图标
|
||||||
|
showIcon: false,
|
||||||
|
},
|
||||||
|
//菜单权限模式 FIXED 前端固定路由 BACK 动态获取
|
||||||
|
permissionMode: 'BACK',
|
||||||
|
//是否开启路由动画
|
||||||
|
isPageAnimate: true,
|
||||||
|
//路由动画类型
|
||||||
|
pageAnimateType: 'fade-slide',
|
||||||
|
};
|
||||||
|
export default setting;
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import projectSetting from '@/settings/projectSetting';
|
||||||
|
import type { ICrumbsSetting, IHeaderSetting, IMenuSetting, IMultiTabsSetting } from '/#/config';
|
||||||
|
|
||||||
|
const {
|
||||||
|
navMode,
|
||||||
|
navTheme,
|
||||||
|
isMobile,
|
||||||
|
headerSetting,
|
||||||
|
showFooter,
|
||||||
|
menuSetting,
|
||||||
|
multiTabsSetting,
|
||||||
|
crumbsSetting,
|
||||||
|
permissionMode,
|
||||||
|
isPageAnimate,
|
||||||
|
pageAnimateType,
|
||||||
|
} = projectSetting;
|
||||||
|
|
||||||
|
interface ProjectSettingState {
|
||||||
|
navMode: string; //导航模式
|
||||||
|
navTheme: string; //导航风格
|
||||||
|
headerSetting: IHeaderSetting; //顶部设置
|
||||||
|
showFooter: boolean; //页脚
|
||||||
|
menuSetting: IMenuSetting; //多标签
|
||||||
|
multiTabsSetting: IMultiTabsSetting; //多标签
|
||||||
|
crumbsSetting: ICrumbsSetting; //面包屑
|
||||||
|
permissionMode: string; //权限模式
|
||||||
|
isPageAnimate: boolean; //是否开启路由动画
|
||||||
|
pageAnimateType: string; //路由动画类型
|
||||||
|
isMobile: boolean; // 是否处于移动端模式
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProjectSettingStore = defineStore({
|
||||||
|
id: 'app-project-setting',
|
||||||
|
state: (): ProjectSettingState => ({
|
||||||
|
navMode: navMode,
|
||||||
|
navTheme,
|
||||||
|
isMobile,
|
||||||
|
headerSetting,
|
||||||
|
showFooter,
|
||||||
|
menuSetting,
|
||||||
|
multiTabsSetting,
|
||||||
|
crumbsSetting,
|
||||||
|
permissionMode,
|
||||||
|
isPageAnimate,
|
||||||
|
pageAnimateType,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
getNavMode(): string {
|
||||||
|
return this.navMode;
|
||||||
|
},
|
||||||
|
getNavTheme(): string {
|
||||||
|
return this.navTheme;
|
||||||
|
},
|
||||||
|
getIsMobile(): boolean {
|
||||||
|
return this.isMobile;
|
||||||
|
},
|
||||||
|
getHeaderSetting(): object {
|
||||||
|
return this.headerSetting;
|
||||||
|
},
|
||||||
|
getShowFooter(): boolean {
|
||||||
|
return this.showFooter;
|
||||||
|
},
|
||||||
|
getMenuSetting(): object {
|
||||||
|
return this.menuSetting;
|
||||||
|
},
|
||||||
|
getMultiTabsSetting(): object {
|
||||||
|
return this.multiTabsSetting;
|
||||||
|
},
|
||||||
|
getCrumbsSetting(): object {
|
||||||
|
return this.crumbsSetting;
|
||||||
|
},
|
||||||
|
getPermissionMode(): string {
|
||||||
|
return this.permissionMode;
|
||||||
|
},
|
||||||
|
getIsPageAnimate(): boolean {
|
||||||
|
return this.isPageAnimate;
|
||||||
|
},
|
||||||
|
getPageAnimateType(): string {
|
||||||
|
return this.pageAnimateType;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setNavTheme(value: string): void {
|
||||||
|
this.navTheme = value;
|
||||||
|
},
|
||||||
|
setIsMobile(value: boolean): void {
|
||||||
|
this.isMobile = value;
|
||||||
|
},
|
||||||
|
setMenuCollapse() {
|
||||||
|
this.menuSetting.collapsed = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,142 @@
|
||||||
|
/* VSCode Dark+ Theme for highlight.js */
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
background: #1e1e1e !important;
|
||||||
|
color: #d4d4d4 !important;
|
||||||
|
--el-scrollbar-opacity: 0.3;
|
||||||
|
--el-scrollbar-bg-color: var(--el-text-color-secondary);
|
||||||
|
--el-scrollbar-hover-opacity: 0.5;
|
||||||
|
--el-scrollbar-hover-bg-color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.hljs::-webkit-scrollbar {
|
||||||
|
width: 6px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--el-scrollbar-bg-color) !important;
|
||||||
|
opacity: var(--el-scrollbar-opacity) !important;
|
||||||
|
border-radius: 3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: var(--el-scrollbar-hover-bg-color) !important;
|
||||||
|
opacity: var(--el-scrollbar-hover-opacity) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword {
|
||||||
|
color: #569cd6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-built_in {
|
||||||
|
color: #4ec9b0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-type {
|
||||||
|
color: #4ec9b0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-literal {
|
||||||
|
color: #569cd6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-number {
|
||||||
|
color: #b5cea8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-regexp {
|
||||||
|
color: #d16969 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string {
|
||||||
|
color: #ce9178 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-subst {
|
||||||
|
color: #d4d4d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-symbol {
|
||||||
|
color: #d4d4d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-class {
|
||||||
|
color: #4ec9b0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-function {
|
||||||
|
color: #dcdcaa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-title {
|
||||||
|
color: #dcdcaa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-params {
|
||||||
|
color: #d4d4d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment {
|
||||||
|
color: #6a9955 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-doctag {
|
||||||
|
color: #608b4e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-meta .hljs-keyword {
|
||||||
|
color: #9b9b9b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-meta .hljs-string {
|
||||||
|
color: #ce9178 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr {
|
||||||
|
color: #9cdcfe !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attribute {
|
||||||
|
color: #9cdcfe !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-name {
|
||||||
|
color: #569cd6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-section {
|
||||||
|
color: #d4d4d4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-tag {
|
||||||
|
color: #569cd6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-variable {
|
||||||
|
color: #9cdcfe !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-template-variable {
|
||||||
|
color: #9cdcfe !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-template-tag {
|
||||||
|
color: #569cd6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加代码块样式 */
|
||||||
|
pre code.hljs {
|
||||||
|
display: block !important;
|
||||||
|
padding: 1em !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
@use './FormCreate/index.scss';
|
@use './FormCreate/index.scss';
|
||||||
@use './theme.scss';
|
@use './theme.scss';
|
||||||
@use 'element-plus/theme-chalk/dark/css-vars.css';
|
@use 'element-plus/theme-chalk/dark/css-vars.css';
|
||||||
|
@import './highligt.css';
|
||||||
.reset-margin [class*='el-icon'] + span {
|
.reset-margin [class*='el-icon'] + span {
|
||||||
margin-left: 2px !important;
|
margin-left: 2px !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function copyToClip(text: string) {
|
||||||
|
return navigator.clipboard
|
||||||
|
.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
return Promise.resolve(text);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
}
|
|
@ -116,3 +116,9 @@ export const isImgPath = (path: string): boolean => {
|
||||||
export const isEmptyVal = (val: any): boolean => {
|
export const isEmptyVal = (val: any): boolean => {
|
||||||
return val === '' || val === null || val === undefined
|
return val === '' || val === null || val === undefined
|
||||||
}
|
}
|
||||||
|
export function isWhitespace(val: unknown) {
|
||||||
|
return val === '';
|
||||||
|
}
|
||||||
|
export function isNullOrWhitespace(val: unknown) {
|
||||||
|
return isNullOrUnDef(val) || isWhitespace(val);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const projectName = import.meta.env.VITE_GLOB_APP_TITLE;
|
||||||
|
|
||||||
|
export function warn(message: string) {
|
||||||
|
console.warn(`[${projectName} warn]:${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function error(message: string) {
|
||||||
|
throw new Error(`[${projectName} error]:${message}`);
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, computed } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Delete, DocumentCopy } from '@element-plus/icons-vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Table } from '@/components/Table'
|
||||||
|
import { useTable } from '@/hooks/web/useTable'
|
||||||
|
import { copyToClip } from '@/utils/copy'
|
||||||
|
import { AppApiManagement } from '@/api/new-ai/appApi'
|
||||||
|
import { hideKey } from '@/api/models/index'
|
||||||
|
import { TableColumn } from '@/types/table'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
channel: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['reload'])
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 表格配置
|
||||||
|
const columns = ref<TableColumn[]>([
|
||||||
|
{
|
||||||
|
label: '密钥',
|
||||||
|
field: 'apiKey',
|
||||||
|
formatter: (row: any) => hideKey(row.apiKey)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
field: 'createTime',
|
||||||
|
width: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '操作',
|
||||||
|
field: 'action',
|
||||||
|
width: 150
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 使用表格Hook
|
||||||
|
const { register, methods, tableObject } = useTable({
|
||||||
|
getListApi: async (params) => {
|
||||||
|
const res = await AppApiManagement.getApiList({
|
||||||
|
...params,
|
||||||
|
appId: router.currentRoute.value.params.id,
|
||||||
|
channel: props.channel
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
list: res.data,
|
||||||
|
total: res.total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 复制API密钥
|
||||||
|
const handleCopy = async (record: any) => {
|
||||||
|
await copyToClip(record.apiKey)
|
||||||
|
ElMessage.success('密钥复制成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除API
|
||||||
|
const handleDelete = (record: any) => {
|
||||||
|
ElMessageBox.confirm('确定要删除该API吗?删除后原Key将立即失效,请谨慎操作', '提示', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
await AppApiManagement.deleteApi(record.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
methods.getList()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增API
|
||||||
|
const handleAdd = async () => {
|
||||||
|
try {
|
||||||
|
await AppApiManagement.createApi({
|
||||||
|
appId: router.currentRoute.value.params.id,
|
||||||
|
channel: props.channel
|
||||||
|
})
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
methods.getList()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('新增失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pagination = computed(() => {
|
||||||
|
return {
|
||||||
|
total: tableObject.total,
|
||||||
|
currentPage: tableObject.currentPage,
|
||||||
|
pageSize: tableObject.pageSize
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 初始加载
|
||||||
|
onMounted(() => {
|
||||||
|
methods.getList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-button type="primary" class="mb-10px" @click="handleAdd">新增密钥</el-button>
|
||||||
|
<Table
|
||||||
|
border
|
||||||
|
:columns="columns"
|
||||||
|
:data="tableObject.tableList"
|
||||||
|
:loading="tableObject.loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
@register="register"
|
||||||
|
>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<el-button :icon="DocumentCopy" text type="primary" @click="handleCopy(row)" />
|
||||||
|
<el-button :icon="Delete" text type="danger" @click="handleDelete(row)" />
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,104 @@
|
||||||
|
<!--
|
||||||
|
- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import PromptPage from './prompt/index.vue'
|
||||||
|
import SettingsPage from './settings/index.vue'
|
||||||
|
import Chat from '@/views/ai/chat/new-chat/Chat.vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useAppStore } from '../store'
|
||||||
|
import { useChatStore } from '@/views/ai/chat/new-chat/store/useChatStore'
|
||||||
|
import { getAppInfo } from '@/api/new-ai/chat'
|
||||||
|
import { formatToDateTime } from '@/utils/dateUtil'
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const chatStore = useChatStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const form = ref<any>({})
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
loading.value = true
|
||||||
|
const id = route.params.id
|
||||||
|
try {
|
||||||
|
const data = await getAppInfo({
|
||||||
|
appId: id,
|
||||||
|
})
|
||||||
|
form.value = data
|
||||||
|
appStore.info = data
|
||||||
|
appStore.knowledgeIds = data.knowledgeIds == null ? [] : data.knowledgeIds
|
||||||
|
appStore.modelId = data.modelId == null ? null : data.modelId
|
||||||
|
appStore.knowledges = data.knowledges == null ? [] : data.knowledges
|
||||||
|
chatStore.modelId = data.modelId == null ? null : data.modelId
|
||||||
|
chatStore.appId = data.id
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取应用信息失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSave() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
form.value.saveTime = formatToDateTime(new Date())
|
||||||
|
await appStore.updateInfo()
|
||||||
|
ElMessage.success('应用配置保存成功')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('保存应用配置失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-container class="h-full" v-loading="loading">
|
||||||
|
<el-aside width="30%" class="h-full">
|
||||||
|
<div class="p-2 h-full bg-white rounded-lg">
|
||||||
|
<PromptPage @update="onSave" />
|
||||||
|
</div>
|
||||||
|
</el-aside>
|
||||||
|
<el-container class="h-full ml-2">
|
||||||
|
<el-aside width="40%" class="h-full">
|
||||||
|
<div class="p-2 h-full bg-white rounded-lg">
|
||||||
|
<SettingsPage @update="onSave" />
|
||||||
|
</div>
|
||||||
|
</el-aside>
|
||||||
|
<el-main class="h-full ml-2">
|
||||||
|
<div class="pb-10 h-full w-full bg-white rounded-xl">
|
||||||
|
<Chat />
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-aside) {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
:deep(.el-main) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,35 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAppStore } from '@/views/ai/app/store';
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
async function onUpdate() {
|
||||||
|
emit('update');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col gap-2">
|
||||||
|
<div class="p-2 flex justify-between items-center">
|
||||||
|
<div class="text-md font-bold">Prompt 提示词</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 pt-0 h-full mb-10" v-if="appStore.info">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
v-model="appStore.info.prompt"
|
||||||
|
class="h-full w-full bg-transparent"
|
||||||
|
@blur="onUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,102 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { nextTick, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { KnowledgeApi } from '@/api/new-ai/knowledge'
|
||||||
|
import { useAppStore } from '@/views/ai/app/store'
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const knowledges = ref()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
async function show() {
|
||||||
|
knowledges.value = await KnowledgeApi.getKnowledgeList({})
|
||||||
|
await nextTick()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAdd(item) {
|
||||||
|
appStore.addKnowledge(item)
|
||||||
|
// ElMessage.success('关联成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRemove(item) {
|
||||||
|
appStore.removeKnowledge(item)
|
||||||
|
ElMessage.success('移除成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
title="关联知识库"
|
||||||
|
width="40%"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
class="w-full mb-2 mt-2"
|
||||||
|
title="注意:只能选择相同纬度向量库配置(以及相同向量模型)的知识库"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-scrollbar height="400px">
|
||||||
|
<el-menu>
|
||||||
|
<el-menu-item v-for="item in knowledges" :key="item.id" class="!px-1">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<div class="flex gap-1 items-center">
|
||||||
|
<el-icon class="text-3xl"><Document /></el-icon>
|
||||||
|
<div>{{ item.name }}</div>
|
||||||
|
|
||||||
|
<el-divider v-if="item.embedModel != null" direction="vertical" />
|
||||||
|
<el-tag v-if="item.embedModel != null" type="success" size="small" round>
|
||||||
|
<div class="flex gap-1 px-1.5">
|
||||||
|
<el-icon><Monitor /></el-icon>
|
||||||
|
<span>{{ item.embedModel.name }}</span>
|
||||||
|
</div>
|
||||||
|
</el-tag>
|
||||||
|
|
||||||
|
<el-divider v-if="item.embedStore != null" direction="vertical" class="!mx-1" />
|
||||||
|
<el-tag v-if="item.embedStore != null" type="primary" size="small" round>
|
||||||
|
<div class="flex gap-1 px-1.5">
|
||||||
|
<el-icon><DataLine /></el-icon>
|
||||||
|
<span>{{ item.embedStore.name }}</span>
|
||||||
|
</div>
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
v-if="!appStore.knowledgeIds.includes(item.id)"
|
||||||
|
class="rounded-2xl py-0 px-6"
|
||||||
|
type="info"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
@click="onAdd(item)"
|
||||||
|
>
|
||||||
|
关联
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
class="rounded-2xl py-0 px-6"
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
@click="onRemove(item)"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||||
|
import KnowledgeList from './KnowledgeList.vue'
|
||||||
|
import ModelSelect from '@/views/common/ModelSelect.vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useAppStore } from '@/views/ai/app/store'
|
||||||
|
import { ArrowRight, ArrowDown } from '@element-plus/icons-vue'
|
||||||
|
const emit = defineEmits(['update'])
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const knowledgeRef = ref()
|
||||||
|
|
||||||
|
async function onSaveModel(val) {
|
||||||
|
appStore.modelId = val.id
|
||||||
|
console.log(val)
|
||||||
|
emit('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShowKbPane() {
|
||||||
|
knowledgeRef.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRemove(item) {
|
||||||
|
appStore.removeKnowledge(item)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-2 py-4 flex flex-col gap-3">
|
||||||
|
<el-collapse :model-value="['0', '1']">
|
||||||
|
<el-collapse-item name="0" title="基础配置">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-24">对话模型:</div>
|
||||||
|
<ModelSelect :id="appStore.modelId" class="w-full" @update="onSaveModel" />
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item name="1" title="知识库">
|
||||||
|
<template #icon="{ isActive }">
|
||||||
|
<div class="flex-1 text-right">
|
||||||
|
<el-button text @click.stop="onShowKbPane" class="flot-right">
|
||||||
|
<SvgIcon class="text-lg" icon="ic:round-plus" />
|
||||||
|
</el-button>
|
||||||
|
<el-icon class="el-collapse-item__arrow " :class="{ 'is-active': isActive }">
|
||||||
|
<ArrowRight />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="appStore.knowledges">
|
||||||
|
<div class="knowledge-list">
|
||||||
|
<div
|
||||||
|
v-for="item in appStore.knowledges"
|
||||||
|
:key="item.id"
|
||||||
|
class="knowledge-item w-full bg-white overflow-hidden rounded-lg hover:bg-gray-100 p-3 mb-2"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex gap-1 items-center">
|
||||||
|
<SvgIcon class="text-3xl" icon="flat-color-icons:document" />
|
||||||
|
<div>{{ item.name }}</div>
|
||||||
|
</div>
|
||||||
|
<el-button text @click="onRemove(item)">
|
||||||
|
<SvgIcon icon="gg:remove" />
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="appStore.knowledges.length == 0" class="text-gray-400 text-md">
|
||||||
|
将文档、URL、三方数据源上传为文本知识库后,用户发送消息时,Bot
|
||||||
|
能够引用文本知识中的内容回答用户问题。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
|
||||||
|
<KnowledgeList ref="knowledgeRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-collapse-item__header {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: #060709cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,148 @@
|
||||||
|
<!--
|
||||||
|
- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import hljs from 'highlight.js';
|
||||||
|
import javascript from 'highlight.js/lib/languages/javascript';
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
hljs.registerLanguage('javascript', javascript);
|
||||||
|
const url = `http://langchat.cn`;
|
||||||
|
const request = `
|
||||||
|
POST /v1/chat/completions HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: 'Bearer YOUR_ACCESS_TOKEN'
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"messages": [
|
||||||
|
{ "role": "user", "content": "你好" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const response = `
|
||||||
|
data: {"choices": [{"index": 0, "delta": {"content": "你好!"}, "finish_reason": null}], "session_id": null}
|
||||||
|
|
||||||
|
data: {"choices": [{"index": 0, "delta": {"content": "我能"}, "finish_reason": null}], "session_id": null}
|
||||||
|
|
||||||
|
data: {"choices": [{"index": 0, "delta": {"content": "为你"}, "finish_reason": null}], "session_id": null}
|
||||||
|
|
||||||
|
data: {"choices": [{"index": 0, "delta": {"content": "做些什么?"}, "finish_reason": null}], "session_id": null}
|
||||||
|
|
||||||
|
data: {"choices": [{"index": 0, "delta": {}, "finish_reason": "stop", "usage": {"prompt_tokens": 9, "completion_tokens": 6, "total_tokens": 15}}], "session_id": null}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const demo = `
|
||||||
|
const url = 'http://langchat.cn/v1/chat/completions';
|
||||||
|
const data = {
|
||||||
|
"messages": [
|
||||||
|
{ "role": "user", "content": "你好" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer YOUR_ACCESS_TOKEN'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok ' + response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(jsonData => {
|
||||||
|
console.log('Success:', jsonData);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 高亮所有代码块
|
||||||
|
document.querySelectorAll('pre code').forEach((el) => {
|
||||||
|
hljs.highlightElement(el as HTMLElement);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-scrollbar class="p-4 bg-white h-full overflow-auto rounded">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<el-alert
|
||||||
|
title="API URL(API接口格式遵循OpenAI格式)"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<div class="bg-[#18181c] mt-10px py-2 px-4 overflow-x-auto rounded">
|
||||||
|
<pre><code class="javascript">{{ url }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-alert
|
||||||
|
title="Request"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-scrollbar class="bg-[#18181c] mt-10px py-2 px-4 overflow-x-auto rounded">
|
||||||
|
<pre><code class="javascript">{{ request }}</code></pre>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-alert
|
||||||
|
title="Response(Stream)"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-scrollbar class="mt-10px bg-[#18181c] py-2 px-4 rounded">
|
||||||
|
<pre><code class="javascript">{{ response }}</code></pre>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<el-alert
|
||||||
|
title="API请求示例"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-scrollbar class="mt-10px bg-[#18181c] py-2 px-4 rounded">
|
||||||
|
<pre><code class="javascript">{{ demo }}</code></pre>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(pre) {
|
||||||
|
margin: 0;
|
||||||
|
code {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!--
|
||||||
|
- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Docs from './components/docs.vue';
|
||||||
|
import ApiTable from '@/views/ai/app/ApiTable.vue';
|
||||||
|
import { CHANNEL } from '@/views/ai/app/columns';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full my-3 pb-8 flex items-start justify-start gap-2 h-full">
|
||||||
|
<div class="bg-white p-4 rounded w-4/5 h-full">
|
||||||
|
<ApiTable :channel="CHANNEL.API" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Docs class="w-full" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,64 @@
|
||||||
|
|
||||||
|
|
||||||
|
import { FormSchema } from '@/types/form';
|
||||||
|
|
||||||
|
export const CHANNEL = {
|
||||||
|
API: 'CHANNEL_API',
|
||||||
|
WEB: 'CHANNEL_WEB',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchSchemas: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'title',
|
||||||
|
component: 'Input',
|
||||||
|
label: '标题',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入Prompt标题查询',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const formSchemas: FormSchema[] = [
|
||||||
|
// {
|
||||||
|
// field: 'id',
|
||||||
|
// label: 'ID',
|
||||||
|
// component: 'Input',
|
||||||
|
// isHidden: true,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
label: '应用名称',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请输入应用名称', trigger: ['blur'] }]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'modelId',
|
||||||
|
label: '关联模型',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请选择关联模型', trigger: ['blur'] }]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'cover',
|
||||||
|
label: '应用封面',
|
||||||
|
component: 'Input',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'des',
|
||||||
|
label: '应用描述',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
isFull: true,
|
||||||
|
placeholder: '请输入应用描述',
|
||||||
|
type: 'textarea',
|
||||||
|
autosize: {
|
||||||
|
minRows: 5,
|
||||||
|
maxRows: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// rules: [{ required: true, message: '请输入应用描述', trigger: ['blur'] }],
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,164 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Form } from '@/components/Form'
|
||||||
|
import { nextTick, ref } from 'vue'
|
||||||
|
import { FormSchema } from '@/types/form'
|
||||||
|
import { AppApi } from '@/api/new-ai/app'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { OssApi } from '@/api/new-ai/oss'
|
||||||
|
import type { UploadFile, UploadRequestOptions } from 'element-plus'
|
||||||
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
|
import ModelSelect from '@/views/common/ModelSelect.vue'
|
||||||
|
const emit = defineEmits(['reload'])
|
||||||
|
const visible = ref(false)
|
||||||
|
const formData = ref({})
|
||||||
|
const formRef = ref()
|
||||||
|
const schemas = ref<Array<FormSchema>>([
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
label: '应用名称',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
required: true,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入应用名称' },
|
||||||
|
{ min: 2, max: 20, message: '长度在2-20个字符' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入应用名称'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'modelId',
|
||||||
|
label: '关联模型',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请选择关联模型' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'cover',
|
||||||
|
label: '应用图标',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入应用图标'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
label: '应用描述',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
required: true,
|
||||||
|
rules: [
|
||||||
|
{ required: true, message: '请输入应用描述' },
|
||||||
|
{ min: 2, max: 200, message: '长度在2-200个字符' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 4,
|
||||||
|
placeholder: '请输入应用描述'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
visible.value = false
|
||||||
|
formRef.value.clearForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = async (data: any = {}) => {
|
||||||
|
visible.value = true
|
||||||
|
isEdit.value = !!data.id
|
||||||
|
await nextTick()
|
||||||
|
if (data.id) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await AppApi.getApp(data.id)
|
||||||
|
formRef.value.setValues(res.data)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取应用详情失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
formRef.value.setValues(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const form = formRef.value.getElFormRef()
|
||||||
|
await form.validate()
|
||||||
|
const values = formRef.value.formModel
|
||||||
|
const Api = isEdit.value ? AppApi.updateApp : AppApi.createApp
|
||||||
|
loading.value = true
|
||||||
|
values.modelId = values.modelId?.[1]
|
||||||
|
await Api(values).finally(() => (loading.value = false))
|
||||||
|
ElMessage.success(isEdit.value ? '更新应用成功' : '创建应用成功')
|
||||||
|
close()
|
||||||
|
emit('reload')
|
||||||
|
}
|
||||||
|
const handleImport = (params: Record<any, any>) => {
|
||||||
|
return {
|
||||||
|
data: params.data.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({
|
||||||
|
show,
|
||||||
|
close
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:close-on-click-modal="!loading"
|
||||||
|
:close-on-press-escape="!loading"
|
||||||
|
draggable
|
||||||
|
:title="isEdit ? '编辑应用' : '新增应用'"
|
||||||
|
width="500px"
|
||||||
|
@close="close"
|
||||||
|
>
|
||||||
|
<Form ref="formRef" :model="formData" :schema="schemas" v-loading="loading">
|
||||||
|
<template #modelId="scope">
|
||||||
|
<ModelSelect :id="scope.modelId" class="w-full" v-model="scope.modelId" />
|
||||||
|
</template>
|
||||||
|
<template #cover="scoped">
|
||||||
|
<UploadImg v-model="scoped.cover" :custom-api="OssApi.upload" :success-before="handleImport"/>
|
||||||
|
</template>
|
||||||
|
</Form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">确认</el-button>
|
||||||
|
<el-button @click="close" :loading="loading">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.avatar-uploader .el-upload {
|
||||||
|
border: 1px dashed var(--el-border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--el-transition-duration-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader .el-upload:hover {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon.avatar-uploader-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #8c939d;
|
||||||
|
width: 178px;
|
||||||
|
height: 178px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,169 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Delete, Edit, InfoFilled, Plus } from '@element-plus/icons-vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { AppApi } from '@/api/new-ai/app'
|
||||||
|
import EditCom from './edit.vue'
|
||||||
|
type TableDataItem = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
apiKey: string
|
||||||
|
provider: string
|
||||||
|
channel: string
|
||||||
|
createTime: string
|
||||||
|
}
|
||||||
|
// 组件实例
|
||||||
|
const editRef = ref()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref<TableDataItem[]>([])
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
const res = await AppApi.getAppList({}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
tableData.value = res || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增应用
|
||||||
|
const handleAdd = () => {
|
||||||
|
editRef.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑应用
|
||||||
|
const handleEdit = (record: Recordable) => {
|
||||||
|
editRef.value?.show(record.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除应用
|
||||||
|
const handleDelete = (record: any) => {
|
||||||
|
ElMessageBox.confirm('确定要删除该应用吗?', '提示', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
await AppApi.deleteApp(record.id)
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleInfo = (record: any) => {
|
||||||
|
router.push({
|
||||||
|
path: `/ai/app/${record.id}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-row :gutter="20" v-loading="loading">
|
||||||
|
<el-col :span="6">
|
||||||
|
<div
|
||||||
|
class="h-full p-10px bg-[#eceef0] transition-all duration-300 hover:border hover:border-blue-400 border border-transparent cursor-pointer rounded-xl group"
|
||||||
|
>
|
||||||
|
<div class="font-bold text-xs mb-1.5 px-6 text-gray-500">创建应用</div>
|
||||||
|
<div
|
||||||
|
class="w-full transition-all hover:bg-white rounded-lg py-1.5 px-6 flex items-center gap-1 font-medium hover:text-blue-500"
|
||||||
|
@click="handleAdd"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<Plus />
|
||||||
|
</el-icon>
|
||||||
|
<span class="text-sm">创建空白应用</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col
|
||||||
|
v-for="item in tableData"
|
||||||
|
:key="item.id"
|
||||||
|
:span="6"
|
||||||
|
>
|
||||||
|
<el-card class="app-card cursor-pointer w-full" shadow="hover" @click="handleInfo(item)">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="sm:mx-4">
|
||||||
|
<div class="relative bg-orange-100 p-4 rounded-lg">
|
||||||
|
<SvgIcon class="text-3xl" icon="prime:microchip-ai" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute bottom-[-6px] p-1 right-[-5px] shadow bg-white mx-auto rounded-lg"
|
||||||
|
>
|
||||||
|
<SvgIcon class="text-sm text-orange-500" icon="lucide:bot" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-lg font-medium">{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="app-card-content flex justify-between items-center">
|
||||||
|
<p class="text-gray-600">{{ item.des || '暂无描述' }}</p>
|
||||||
|
<el-dropdown trigger="hover" width="500px">
|
||||||
|
<div
|
||||||
|
:class="[activeDropdownId === item.id ? 'bg-gray-200' : 'hover:bg-gray-200']"
|
||||||
|
class="rounded p-1 transition-all"
|
||||||
|
>
|
||||||
|
<SvgIcon class="w-5 h-5" icon="ri:more-fill" />
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click.stop="handleEdit(item)">编辑此应用</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click.stop="handleDelete(item)">删除此应用</el-dropdown-item>
|
||||||
|
<el-dropdown-item disabled divided>
|
||||||
|
<div class="w-150px">
|
||||||
|
<div>信息</div>
|
||||||
|
<span class="text-xs text-stone-500"> 创建时间:{{ item.createTime }} </span>
|
||||||
|
</div>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col v-if="!tableData.length" :span="18">
|
||||||
|
<el-empty description="暂无数据"/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<EditCom ref="editRef" @reload="loadData" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.group:hover {
|
||||||
|
border: 1px solid var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.app-card {
|
||||||
|
height: 100%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-col {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,96 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import AppBase from './base/index.vue';
|
||||||
|
import ApiChannel from './channel-api/index.vue';
|
||||||
|
// import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useAppStore } from './store';
|
||||||
|
import { getAppInfo } from '@/api/new-ai/chat';
|
||||||
|
|
||||||
|
const appStore = useAppStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const form = ref<any>({name: 123});
|
||||||
|
const loading = ref(false);
|
||||||
|
const activeMenus = [
|
||||||
|
{ key: 'setting', icon: 'uil:setting', label: '应用配置' },
|
||||||
|
{ key: 'api', icon: 'hugeicons:api', label: 'API接入渠道' },
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
loading.value = true;
|
||||||
|
const appId = route.params.id;
|
||||||
|
const data = await getAppInfo({ appId }).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
});
|
||||||
|
form.value = data;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="form.name !== undefined" class="rounded bg-[#f9f9f9] w-full h-full pb-10 children">
|
||||||
|
<div class="p-4 flex justify-between items-center bg-white rounded">
|
||||||
|
<div class="flex gap-5 items-center min-w-20">
|
||||||
|
<el-button text type="primary" @click="router.push('/ai/app')">
|
||||||
|
<SvgIcon class="text-xl" icon="icon-park-outline:back" />
|
||||||
|
</el-button>
|
||||||
|
<div class="flex gap-2 items-center pr-4">
|
||||||
|
<div class="mr-3">
|
||||||
|
<div class="relative bg-orange-100 p-4 rounded-lg">
|
||||||
|
<SvgIcon class="text-3xl" icon="prime:microchip-ai" />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute bottom-[-6px] p-1 right-[-5px] shadow bg-white mx-auto rounded-lg"
|
||||||
|
>
|
||||||
|
<SvgIcon class="text-sm text-orange-500" icon="lucide:bot" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-between gap-2">
|
||||||
|
<div class="font-bold text-lg">{{ form.name }}</div>
|
||||||
|
<div v-if="!loading" class="text-gray-400 text-xs">自动保存:{{ form.saveTime }}</div>
|
||||||
|
<div v-else class="flex items-center gap-1 text-gray-400 text-xs">
|
||||||
|
<SvgIcon icon="eos-icons:bubble-loading" />保存中...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-button
|
||||||
|
v-for="item in activeMenus"
|
||||||
|
:key="item.key"
|
||||||
|
:type="appStore.activeMenu === item.key ? 'primary' : 'default'"
|
||||||
|
class="!px-5 !rounded-2xl"
|
||||||
|
secondary
|
||||||
|
strong
|
||||||
|
@click="appStore.setActiveMenu(item.key)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon :icon="item.icon" />
|
||||||
|
</template>
|
||||||
|
{{ item.label }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AppBase v-if="appStore.activeMenu === 'setting'" />
|
||||||
|
<ApiChannel v-if="appStore.activeMenu === 'api'" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.children {
|
||||||
|
height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - (var(--app-content-padding) * 4)) !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
&>* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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 { defineStore } from 'pinia';
|
||||||
|
import { AppApi } from '@/api/new-ai/app';
|
||||||
|
|
||||||
|
export interface AppState {
|
||||||
|
activeMenu: string;
|
||||||
|
info: any;
|
||||||
|
modelId: string | null;
|
||||||
|
knowledgeIds: any[];
|
||||||
|
knowledges: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app-store', {
|
||||||
|
state: (): AppState =>
|
||||||
|
<AppState>{
|
||||||
|
activeMenu: 'setting',
|
||||||
|
info: {},
|
||||||
|
modelId: '',
|
||||||
|
knowledgeIds: [],
|
||||||
|
knowledges: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
getters: {},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
setActiveMenu(active: string) {
|
||||||
|
this.activeMenu = active;
|
||||||
|
},
|
||||||
|
addKnowledge(item: any) {
|
||||||
|
this.knowledgeIds.push(item.id);
|
||||||
|
this.knowledges.push(item);
|
||||||
|
this.updateInfo();
|
||||||
|
},
|
||||||
|
|
||||||
|
removeKnowledge(item: any) {
|
||||||
|
this.knowledgeIds = this.knowledgeIds.filter((i) => i !== item.id);
|
||||||
|
this.knowledges = this.knowledges.filter((i) => i.id !== item.id);
|
||||||
|
this.updateInfo();
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateInfo() {
|
||||||
|
this.info.modelId = this.modelId;
|
||||||
|
this.info.knowledgeIds = this.knowledgeIds;
|
||||||
|
this.info.knowledges = this.knowledges;
|
||||||
|
await AppApi.updateApp({ ...this.info });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,244 @@
|
||||||
|
<!--
|
||||||
|
- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Message from '@/views/ai/chat/new-chat/message/Message.vue'
|
||||||
|
import { useProjectSetting } from '@/hooks/setting/useProjectSetting'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useChatStore } from './store/useChatStore'
|
||||||
|
import { useScroll } from './store/useScroll'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { CircleClose, Position } from '@element-plus/icons-vue'
|
||||||
|
import { chat } from '@/api/new-ai/chat'
|
||||||
|
|
||||||
|
const ms = ElMessage
|
||||||
|
const chatStore = useChatStore()
|
||||||
|
const { scrollRef, contentRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll()
|
||||||
|
const { isMobile } = useProjectSetting()
|
||||||
|
const loading = ref<boolean>(false)
|
||||||
|
const message = ref('')
|
||||||
|
const chatId = ref<string>('')
|
||||||
|
const aiChatId = ref<string>('')
|
||||||
|
let controller = new AbortController()
|
||||||
|
|
||||||
|
const footerClass = computed(() => {
|
||||||
|
let classes = ['p-4']
|
||||||
|
if (isMobile.value) {
|
||||||
|
classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'pr-3', 'overflow-hidden']
|
||||||
|
}
|
||||||
|
return classes
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataSources = computed(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
return chatStore.messages
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleEnter(event: KeyboardEvent) {
|
||||||
|
if (!isMobile.value) {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
handleSubmit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (event.key === 'Enter' && event.ctrlKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
handleSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const msg = message.value
|
||||||
|
if (loading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!msg || msg.trim() === '') {
|
||||||
|
ms.error('请先输入消息内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller = new AbortController()
|
||||||
|
|
||||||
|
// user
|
||||||
|
chatId.value = uuidv4()
|
||||||
|
await chatStore.addMessage(msg, 'user', chatId.value)
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
message.value = ''
|
||||||
|
|
||||||
|
// ai
|
||||||
|
await scrollToBottom()
|
||||||
|
aiChatId.value = uuidv4()
|
||||||
|
await scrollToBottom()
|
||||||
|
await chatStore.addMessage('', 'assistant', aiChatId.value)
|
||||||
|
await scrollToBottomIfAtBottom()
|
||||||
|
|
||||||
|
await onChat(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onChat(message: string) {
|
||||||
|
try {
|
||||||
|
await chat(
|
||||||
|
{
|
||||||
|
chatId: chatId.value,
|
||||||
|
conversationId: chatStore.conversationId,
|
||||||
|
appId: chatStore.appId,
|
||||||
|
message,
|
||||||
|
role: 'user',
|
||||||
|
modelId: chatStore.modelId,
|
||||||
|
modelName: chatStore.modelName,
|
||||||
|
modelProvider: chatStore.modelProvider,
|
||||||
|
},
|
||||||
|
controller,
|
||||||
|
async ({ event }) => {
|
||||||
|
const list = event.target.responseText.split('\n\n')
|
||||||
|
|
||||||
|
let text = ''
|
||||||
|
let isRun = true
|
||||||
|
list.forEach((i: any) => {
|
||||||
|
if (i.startsWith('data:Error')) {
|
||||||
|
isRun = false
|
||||||
|
text += i.substring(5, i.length)
|
||||||
|
chatStore.updateMessage(aiChatId.value, text, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!i.startsWith('data:{')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { done, message } = JSON.parse(i.substring(5, i.length))
|
||||||
|
if (done || message === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text += message
|
||||||
|
})
|
||||||
|
if (!isRun) {
|
||||||
|
await scrollToBottomIfAtBottom()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await chatStore.updateMessage(aiChatId.value, text, false)
|
||||||
|
await scrollToBottomIfAtBottom()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((e: any) => {
|
||||||
|
loading.value = false
|
||||||
|
console.error('chat error', e)
|
||||||
|
if (e.message !== undefined) {
|
||||||
|
chatStore.updateMessage(aiChatId.value, e.message || 'chat error', true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.startsWith('data:Error')) {
|
||||||
|
chatStore.updateMessage(aiChatId.value, e.substring(5, e.length), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
scrollToBottomIfAtBottom()
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStop() {
|
||||||
|
if (loading.value) {
|
||||||
|
controller.abort()
|
||||||
|
controller = new AbortController()
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(item: any) {
|
||||||
|
if (loading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessageBox.confirm('确认删除消息', '删除消息', {
|
||||||
|
confirmButtonText: '是',
|
||||||
|
cancelButtonText: '否',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
chatStore.delMessage(item)
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-full h-full">
|
||||||
|
<main class="flex-1 overflow-hidden">
|
||||||
|
<el-scrollbar ref="contentRef" class="h-full overflow-hidden overflow-y-auto">
|
||||||
|
<div
|
||||||
|
ref="scrollRef"
|
||||||
|
:class="[isMobile ? 'p-2' : 'p-5']"
|
||||||
|
class="w-full max-w-screen-3xl m-auto"
|
||||||
|
>
|
||||||
|
<Message
|
||||||
|
v-for="(item, index) of dataSources"
|
||||||
|
:key="index"
|
||||||
|
:class="dataSources.length - 1 == index ? '!mb-2' : 'mb-6'"
|
||||||
|
:date-time="item.createTime"
|
||||||
|
:error="item.isError"
|
||||||
|
:inversion="item.role !== 'assistant'"
|
||||||
|
:loading="loading"
|
||||||
|
:text="item.message"
|
||||||
|
@delete="handleDelete(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer :class="footerClass">
|
||||||
|
<div class="w-full max-w-screen-3xl m-auto">
|
||||||
|
<div class="flex items-center justify-between space-x-2">
|
||||||
|
<el-input
|
||||||
|
v-model="message"
|
||||||
|
:autosize="{ minRows: 1, maxRows: isMobile ? 4 : 8 }"
|
||||||
|
:placeholder="loading ? '等待响应中...' : '请输入消息内容'"
|
||||||
|
:disabled="loading"
|
||||||
|
type="textarea"
|
||||||
|
@keypress="handleEnter"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<el-button v-if="loading" type="danger" @click="handleStop">
|
||||||
|
<template #icon>
|
||||||
|
<el-icon><CircleClose /></el-icon>
|
||||||
|
</template>
|
||||||
|
停止
|
||||||
|
</el-button>
|
||||||
|
<el-button v-else type="primary" @click="handleSubmit">
|
||||||
|
<template #icon>
|
||||||
|
<el-icon><Position /></el-icon>
|
||||||
|
</template>
|
||||||
|
发送
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-textarea__inner) {
|
||||||
|
padding: 8px 12px;
|
||||||
|
min-height: 42px;
|
||||||
|
max-height: 200px;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,60 @@
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Delete, Star } from '@element-plus/icons-vue'
|
||||||
|
import { useChatStore } from './store/useChatStore'
|
||||||
|
import { clean } from '@/api/new-ai/chat'
|
||||||
|
import ModelSelect from '@/views/common/ModelSelect.vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits(['reload'])
|
||||||
|
const chatStore = useChatStore()
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
if (chatStore.conversationId == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessageBox.confirm('确认清除聊天', '清除聊天', {
|
||||||
|
confirmButtonText: '是',
|
||||||
|
cancelButtonText: '否',
|
||||||
|
type: 'warning',
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
await clean(chatStore.conversationId!)
|
||||||
|
emits('reload')
|
||||||
|
ElMessage.success('聊天记录清除成功')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('清除聊天记录失败')
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mb-10px flex flex-wrap justify-between items-center pl-10px">
|
||||||
|
<div class="font-bold flex justify-center items-center flex-wrap gap-2">
|
||||||
|
<SvgIcon class="text-lg" icon="ion:sparkles-outline" />
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ModelSelect :id="chatStore.modelId" class="w-auto min-w-[180px]" />
|
||||||
|
|
||||||
|
<el-button type="warning" plain @click="handleClear">
|
||||||
|
<template #icon>
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</template>
|
||||||
|
清空聊天
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import Chat from '@/views/ai/chat/new-chat/Chat.vue';
|
||||||
|
import { getMessages } from '@/api/new-ai/chat';
|
||||||
|
import { useChatStore } from '@/views/ai/chat/new-chat/store/useChatStore';
|
||||||
|
import { useUserStore } from '@/store/modules/user';
|
||||||
|
import Header from './Header.vue';
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetch();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetch() {
|
||||||
|
loading.value = true;
|
||||||
|
chatStore.conversationId = userStore.user.id;
|
||||||
|
chatStore.messages = await getMessages(userStore.user.id);
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-4 pt-1 chat-card w-full h-full bg-white rounded-xl shadow-md pt-20px">
|
||||||
|
<Header title="AI聊天助手" @reload="fetch" />
|
||||||
|
<main ref="contentRef" class="flex-1 overflow-hidden overflow-y-auto">
|
||||||
|
<Chat />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.chat-card {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - (var(--app-content-padding) * 3) + 20px) !important;
|
||||||
|
main {
|
||||||
|
height: calc(100% - 60px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,129 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import TextComponent from './TextComponent.vue'
|
||||||
|
import { copyToClip } from '@/utils/copy'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dateTime?: string
|
||||||
|
text?: string
|
||||||
|
inversion?: boolean
|
||||||
|
error?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emit {
|
||||||
|
(ev: 'delete'): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<Emit>()
|
||||||
|
const isHover = ref(false)
|
||||||
|
const textRef = ref<HTMLElement>()
|
||||||
|
const asRawText = ref(props.inversion)
|
||||||
|
const messageRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
const options = computed(() => {
|
||||||
|
const common = [
|
||||||
|
{
|
||||||
|
label: '复制',
|
||||||
|
key: 'copyText',
|
||||||
|
icon: 'DocumentCopy'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!props.inversion) {
|
||||||
|
common.push({
|
||||||
|
label: asRawText.value ? '预览' : '显示原文',
|
||||||
|
key: 'toggleRenderType',
|
||||||
|
icon: asRawText.value ? 'View' : 'Document'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return common
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSelect(key: 'copyText' | 'delete' | 'toggleRenderType') {
|
||||||
|
switch (key) {
|
||||||
|
case 'copyText':
|
||||||
|
handleCopy()
|
||||||
|
return
|
||||||
|
case 'toggleRenderType':
|
||||||
|
asRawText.value = !asRawText.value
|
||||||
|
return
|
||||||
|
case 'delete':
|
||||||
|
emit('delete')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopy() {
|
||||||
|
try {
|
||||||
|
await copyToClip(props.text || '')
|
||||||
|
ElMessage.success('复制成功')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="messageRef"
|
||||||
|
:class="[{ 'flex-row-reverse': inversion }]"
|
||||||
|
class="flex w-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[inversion ? 'ml-2' : 'mr-2']"
|
||||||
|
class="flex items-center justify-center bg-gray-200 flex-shrink-0 h-8 overflow-hidden rounded-full basis-8"
|
||||||
|
>
|
||||||
|
<SvgIcon v-if="inversion" icon="solar:user-broken" />
|
||||||
|
<SvgIcon v-else icon="mingcute:ai-line" />
|
||||||
|
</div>
|
||||||
|
<div :class="[inversion ? 'items-end' : 'items-start']" class="overflow-hidden text-sm">
|
||||||
|
<p :class="[inversion ? 'text-right' : 'text-left']" class="text-xs text-[#b4bbc4]">
|
||||||
|
{{ dateTime }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
@mouseover="isHover = true"
|
||||||
|
@mouseleave="isHover = false"
|
||||||
|
:class="[inversion ? 'flex-row-reverse' : 'flex-row']"
|
||||||
|
class="flex items-end gap-1 mt-2 transition-all"
|
||||||
|
>
|
||||||
|
<TextComponent
|
||||||
|
ref="textRef"
|
||||||
|
:as-raw-text="asRawText"
|
||||||
|
:error="error"
|
||||||
|
:inversion="inversion"
|
||||||
|
:loading="loading"
|
||||||
|
:text="text"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col transition-all w-[45px]">
|
||||||
|
<div v-if="isHover" class="flex gap-1.5 flex-nowrap justify-end">
|
||||||
|
<el-tooltip
|
||||||
|
v-for="item in options"
|
||||||
|
:key="item.key"
|
||||||
|
:content="item.label"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
:icon="item.icon"
|
||||||
|
@click="handleSelect(item.key as any)"
|
||||||
|
class="transition text-neutral-400 hover:text-neutral-800"
|
||||||
|
/>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-button) {
|
||||||
|
padding: 4px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,124 @@
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import mdKatex from '@traptitech/markdown-it-katex'
|
||||||
|
import mila from 'markdown-it-link-attributes'
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
import { copyToClip } from '@/utils/copy'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
inversion?: boolean
|
||||||
|
error?: boolean
|
||||||
|
text?: string
|
||||||
|
loading?: boolean
|
||||||
|
asRawText?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const isMobile = ref(false) // TODO: 从全局状态获取
|
||||||
|
const textRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
const mdi = new MarkdownIt({
|
||||||
|
html: false,
|
||||||
|
linkify: true,
|
||||||
|
highlight(code, language) {
|
||||||
|
const validLang = !!(language && hljs.getLanguage(language))
|
||||||
|
if (validLang) {
|
||||||
|
const lang = language ?? ''
|
||||||
|
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)
|
||||||
|
}
|
||||||
|
return highlightBlock(hljs.highlightAuto(code).value, '')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
|
||||||
|
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
|
||||||
|
|
||||||
|
const wrapClass = computed(() => {
|
||||||
|
return [
|
||||||
|
'text-wrap',
|
||||||
|
'rounded-md',
|
||||||
|
isMobile.value ? 'p-2' : 'px-3 py-2',
|
||||||
|
props.inversion ? 'bg-[#70c0e829]' : 'bg-[#f4f6f8]',
|
||||||
|
props.inversion ? 'message-request' : 'message-reply',
|
||||||
|
{ 'text-red-500': props.error },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = computed(() => {
|
||||||
|
const value = props.text ?? ''
|
||||||
|
if (!props.asRawText) {
|
||||||
|
return mdi.render(value)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
|
||||||
|
function highlightBlock(str: string, lang?: string) {
|
||||||
|
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCopyEvents() {
|
||||||
|
if (textRef.value) {
|
||||||
|
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
|
||||||
|
copyBtn.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const code = btn.parentElement?.nextElementSibling?.textContent
|
||||||
|
if (code) {
|
||||||
|
copyToClip(code).then(() => {
|
||||||
|
ElMessage.success('复制成功')
|
||||||
|
const btnEl = btn as HTMLElement
|
||||||
|
btnEl.textContent = '复制成功'
|
||||||
|
setTimeout(() => {
|
||||||
|
btnEl.textContent = '复制'
|
||||||
|
}, 1000)
|
||||||
|
}).catch(() => {
|
||||||
|
ElMessage.error('复制失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCopyEvents() {
|
||||||
|
if (textRef.value) {
|
||||||
|
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
|
||||||
|
copyBtn.forEach((btn) => {
|
||||||
|
btn.removeEventListener('click', () => {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
addCopyEvents()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
addCopyEvents()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
removeCopyEvents()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="wrapClass" class="text-black">
|
||||||
|
<div ref="textRef" class="leading-relaxed break-words">
|
||||||
|
<div v-if="!inversion">
|
||||||
|
<div v-if="!asRawText" class="markdown-body" v-html="text"></div>
|
||||||
|
<div v-else class="whitespace-pre-wrap" v-text="text"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="whitespace-pre-wrap" v-text="text"></div>
|
||||||
|
<template v-if="loading && !text">
|
||||||
|
<span class="w-[4px] h-[20px] block animate-blink"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import './styles/index.scss';
|
||||||
|
</style>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,208 @@
|
||||||
|
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
pre code.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.hljs {
|
||||||
|
padding: 3px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
color: #abb2bf;
|
||||||
|
background: #282c34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-operator,
|
||||||
|
.hljs-pattern-match {
|
||||||
|
color: #f92672;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-function,
|
||||||
|
.hljs-pattern-match .hljs-constructor {
|
||||||
|
color: #61aeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-function .hljs-params {
|
||||||
|
color: #a6e22e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-function .hljs-params .hljs-typing {
|
||||||
|
color: #fd971f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-module-access .hljs-module {
|
||||||
|
color: #7e57c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-constructor {
|
||||||
|
color: #e2b93d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-constructor .hljs-string {
|
||||||
|
color: #9ccc65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #b18eb1;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-formula {
|
||||||
|
color: #c678dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-subst {
|
||||||
|
color: #e06c75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-literal {
|
||||||
|
color: #56b6c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-meta .hljs-string,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-string {
|
||||||
|
color: #98c379;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-class .hljs-title,
|
||||||
|
.hljs-title.class_ {
|
||||||
|
color: #e6c07b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-variable {
|
||||||
|
color: #d19a66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-title {
|
||||||
|
color: #61aeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
pre code.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.hljs {
|
||||||
|
padding: 3px 5px;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
color: #383a42;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #a0a1a7;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-formula,
|
||||||
|
.hljs-keyword {
|
||||||
|
color: #a626a4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-subst {
|
||||||
|
color: #e45649;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-literal {
|
||||||
|
color: #0184bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-meta .hljs-string,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-string {
|
||||||
|
color: #50a14f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-variable {
|
||||||
|
color: #986801;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-title {
|
||||||
|
color: #4078f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-class .hljs-title,
|
||||||
|
.hljs-title.class_ {
|
||||||
|
color: #c18401;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
@import "github-markdown";
|
||||||
|
@import "highlight";
|
||||||
|
@import "style";
|
|
@ -0,0 +1,89 @@
|
||||||
|
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code,
|
||||||
|
pre tt {
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight pre,
|
||||||
|
pre {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.hljs {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
&-wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 24px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
color: #b3b3b3;
|
||||||
|
|
||||||
|
&__copy {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #65a665;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
.message-reply {
|
||||||
|
.whitespace-pre-wrap {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: var(--n-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight pre,
|
||||||
|
pre {
|
||||||
|
background-color: #282c34;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, to {
|
||||||
|
background-color: currentColor;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-blink {
|
||||||
|
animation: blink 1.2s infinite steps(1, start);
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ChatState {
|
||||||
|
messages: any[];
|
||||||
|
modelId: string | null;
|
||||||
|
modelName: string | null;
|
||||||
|
modelProvider: string | null;
|
||||||
|
conversationId: string | null;
|
||||||
|
appId: any;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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 { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
|
||||||
|
|
||||||
|
export const useBasicLayout = () => {
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||||
|
const isMobile = breakpoints.smaller('sm');
|
||||||
|
return { isMobile };
|
||||||
|
};
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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 { defineStore } from 'pinia';
|
||||||
|
import { formatToDateTime } from '@/utils/dateUtil';
|
||||||
|
import type { ChatState } from '@/views/ai/chat/new-chat/store/chat';
|
||||||
|
|
||||||
|
export const useChatStore = defineStore('chat-store', {
|
||||||
|
state: (): ChatState =>
|
||||||
|
<ChatState>{
|
||||||
|
modelId: null,
|
||||||
|
modelName: '',
|
||||||
|
modelProvider: '',
|
||||||
|
conversationId: null,
|
||||||
|
messages: [],
|
||||||
|
appId: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
getters: {},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* 新增消息
|
||||||
|
*/
|
||||||
|
async addMessage(
|
||||||
|
message: string,
|
||||||
|
role: 'user' | 'assistant' | 'system',
|
||||||
|
chatId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
this.messages.push({
|
||||||
|
chatId,
|
||||||
|
role: role,
|
||||||
|
message: message,
|
||||||
|
createTime: formatToDateTime(new Date()),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新消息
|
||||||
|
* chatId 仅仅用于更新流式消息内容
|
||||||
|
*/
|
||||||
|
async updateMessage(chatId: string, message: string, isError?: boolean) {
|
||||||
|
const index = this.messages.findIndex((item) => item?.chatId == chatId);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.messages[index].message = message;
|
||||||
|
this.messages[index].isError = isError;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息
|
||||||
|
*/
|
||||||
|
async delMessage(item: any) {
|
||||||
|
this.messages = this.messages.filter((i) => i.promptId !== item.promptId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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 { h } from 'vue';
|
||||||
|
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||||
|
|
||||||
|
export const useIconRender = () => {
|
||||||
|
interface IconConfig {
|
||||||
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IconStyle {
|
||||||
|
color?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconRender = (config: IconConfig) => {
|
||||||
|
const { color, fontSize, icon } = config;
|
||||||
|
|
||||||
|
const style: IconStyle = {};
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
style.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fontSize) {
|
||||||
|
style.fontSize = `${fontSize}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!icon) {
|
||||||
|
window.console.warn('iconRender: icon is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => h(SvgIcon, { icon, style });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
iconRender,
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* 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 type { Ref } from 'vue'
|
||||||
|
import { nextTick, ref } from 'vue'
|
||||||
|
|
||||||
|
type ScrollElement = HTMLDivElement | null
|
||||||
|
|
||||||
|
interface ScrollReturn {
|
||||||
|
contentRef: Ref<ScrollElement>
|
||||||
|
scrollRef: Ref<ScrollElement>
|
||||||
|
scrollToBottom: () => Promise<void>
|
||||||
|
scrollToTop: () => Promise<void>
|
||||||
|
scrollToBottomIfAtBottom: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useScroll(): ScrollReturn {
|
||||||
|
const scrollRef = ref<ScrollElement>(null)
|
||||||
|
const contentRef = ref<ScrollElement>(null)
|
||||||
|
|
||||||
|
const scrollToBottom = async () => {
|
||||||
|
await nextTick()
|
||||||
|
if (contentRef.value) {
|
||||||
|
contentRef.value.scrollTo({ top: contentRef.value.scrollHeight, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToTop = async () => {
|
||||||
|
await nextTick()
|
||||||
|
if (contentRef.value) {
|
||||||
|
contentRef.value.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToBottomIfAtBottom = async () => {
|
||||||
|
await nextTick()
|
||||||
|
if (contentRef.value) {
|
||||||
|
const threshold = 100 // 阈值,表示滚动条到底部的距离阈值
|
||||||
|
const distanceToBottom =
|
||||||
|
contentRef.value.scrollHeight - contentRef.value.scrollTop - contentRef.value.clientHeight
|
||||||
|
if (distanceToBottom <= threshold) {
|
||||||
|
contentRef.value.scrollTo({ top: contentRef.value.scrollHeight, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollRef,
|
||||||
|
contentRef,
|
||||||
|
scrollToBottom,
|
||||||
|
scrollToTop,
|
||||||
|
scrollToBottomIfAtBottom,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
|
||||||
|
|
||||||
|
import { TableColumnCtx } from 'element-plus';
|
||||||
|
import { ElTag } from 'element-plus';
|
||||||
|
import { h } from 'vue';
|
||||||
|
import type { FormSchema } from '@/types/form';
|
||||||
|
export const columns: Partial<TableColumnCtx<any>>[] = [
|
||||||
|
{
|
||||||
|
label: '用户名',
|
||||||
|
prop: 'username',
|
||||||
|
align: 'center',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '请求ip',
|
||||||
|
prop: 'ip',
|
||||||
|
align: 'center',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '对话角色',
|
||||||
|
prop: 'role',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{
|
||||||
|
type: row.role === 'user' ? 'success' : 'danger',
|
||||||
|
size: 'small',
|
||||||
|
},
|
||||||
|
{ default: () => row.role }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '模型名称',
|
||||||
|
prop: 'model',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Token消耗',
|
||||||
|
prop: 'tokens',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '提示词Token消耗',
|
||||||
|
prop: 'promptTokens',
|
||||||
|
align: 'center',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '消息内容',
|
||||||
|
prop: 'message',
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return String(row.message).replace(/```|\n/g, '');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '会话时间',
|
||||||
|
prop: 'createTime',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const searchSchemas: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'text',
|
||||||
|
component: 'Input',
|
||||||
|
label: '内容',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入内容查询',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'username',
|
||||||
|
component: 'Input',
|
||||||
|
label: '用户名',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户名查询',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'role',
|
||||||
|
component: 'Select',
|
||||||
|
label: '对话角色',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择对话角色查询',
|
||||||
|
style: {
|
||||||
|
width: '140px',
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'user',
|
||||||
|
value: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'assistant',
|
||||||
|
value: 'assistant',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { FormSchema } from '@/types/form'
|
||||||
|
import type { TableColumn } from '@/types/table'
|
||||||
|
import { useTable } from '@/hooks/web/useTable'
|
||||||
|
import { ElTag, dayjs } from 'element-plus'
|
||||||
|
import { h } from 'vue'
|
||||||
|
import * as MessageApi from '@/api/new-ai/message'
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const searchParams = ref({})
|
||||||
|
|
||||||
|
const schema = ref<FormSchema[]>([
|
||||||
|
{
|
||||||
|
field: 'text',
|
||||||
|
component: 'Input',
|
||||||
|
label: '内容',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入内容查询'
|
||||||
|
},
|
||||||
|
colProps: {
|
||||||
|
span: 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'username',
|
||||||
|
component: 'Input',
|
||||||
|
label: '用户名',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户名查询'
|
||||||
|
},
|
||||||
|
colProps: {
|
||||||
|
span: 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'role',
|
||||||
|
component: 'Select',
|
||||||
|
label: '对话角色',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择对话角色查询',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'user',
|
||||||
|
value: 'user'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'assistant',
|
||||||
|
value: 'assistant'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
colProps: {
|
||||||
|
span: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const columns = ref<TableColumn[]>([
|
||||||
|
{
|
||||||
|
label: '用户名',
|
||||||
|
field: 'username',
|
||||||
|
align: 'center',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '请求ip',
|
||||||
|
field: 'ip',
|
||||||
|
align: 'center',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '对话角色',
|
||||||
|
field: 'role',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
formatter(row) {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{
|
||||||
|
type: row.role === 'user' ? 'success' : 'danger',
|
||||||
|
size: 'small'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => row.role
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '模型名称',
|
||||||
|
field: 'model',
|
||||||
|
align: 'center',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Token消耗',
|
||||||
|
field: 'tokens',
|
||||||
|
align: 'center',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '提示词Token消耗',
|
||||||
|
field: 'promptTokens',
|
||||||
|
align: 'center',
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '消息内容',
|
||||||
|
field: 'message',
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return String(row.message).replace(/```|\n/g, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '会话时间',
|
||||||
|
field: 'createTime',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
])
|
||||||
|
|
||||||
|
const { register, tableObject, methods } = useTable({
|
||||||
|
getListApi: MessageApi.page,
|
||||||
|
defaultParams: searchParams.value,
|
||||||
|
delListApi: MessageApi.del
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearch = (values: any) => {
|
||||||
|
methods.setSearchParams(values)
|
||||||
|
// methods.getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagination = computed(() => {
|
||||||
|
return {
|
||||||
|
total: tableObject.total,
|
||||||
|
pageSize: tableObject.pageSize,
|
||||||
|
currentPage: tableObject.currentPage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleDel = async (id: string | number) => {
|
||||||
|
try {
|
||||||
|
await methods.delList(id, false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete message:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
methods.getList()
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
schema,
|
||||||
|
columns,
|
||||||
|
register,
|
||||||
|
handleSearch,
|
||||||
|
methods,
|
||||||
|
tableObject,
|
||||||
|
pagination,
|
||||||
|
handleDel
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { FormSchema } from '@/types/form'
|
||||||
|
|
||||||
|
export const searchSchemas: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'text',
|
||||||
|
component: 'Input',
|
||||||
|
label: '内容',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入内容'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,68 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { nextTick, ref } from 'vue';
|
||||||
|
import { getMessages } from '@/api/new-ai/conversation';
|
||||||
|
import Message from '@/views/ai/chat/new-chat/message/Message.vue';
|
||||||
|
|
||||||
|
const messageRef = ref();
|
||||||
|
const contentRef = ref();
|
||||||
|
const loading = ref(true);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const info = ref<any>({});
|
||||||
|
const messages = ref<any>([]);
|
||||||
|
|
||||||
|
async function show(row: any) {
|
||||||
|
dialogVisible.value = true;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
info.value = row;
|
||||||
|
messages.value = await getMessages(row.id);
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(row) {
|
||||||
|
console.log('del', row);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="info.title"
|
||||||
|
size="1000px"
|
||||||
|
direction="rtl"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div ref="contentRef" class="drawer-content">
|
||||||
|
<el-scrollbar ref="messageRef" height="calc(100vh - 180px)">
|
||||||
|
<Message
|
||||||
|
v-for="(item, index) of messages"
|
||||||
|
:key="index"
|
||||||
|
:date-time="item.createTime"
|
||||||
|
:error="false"
|
||||||
|
:inversion="item.role !== 'assistant'"
|
||||||
|
:loading="loading"
|
||||||
|
:text="item.message"
|
||||||
|
@delete="handleDelete(item)"
|
||||||
|
/>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="messages.length === 0" description="此会话还没有聊天信息" class="mt-5" />
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div style="flex: auto">
|
||||||
|
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.drawer-content {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import type { FormSchema } from '@/types/form'
|
||||||
|
import type { TableColumn } from '@/types/table'
|
||||||
|
import { useTable } from '@/hooks/web/useTable'
|
||||||
|
import { ElTag, dayjs } from 'element-plus'
|
||||||
|
import { h } from 'vue'
|
||||||
|
import * as ConversationApi from '@/api/new-ai/conversation'
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const searchParams = ref({})
|
||||||
|
const infoRef = ref()
|
||||||
|
|
||||||
|
const schema = ref<FormSchema[]>([
|
||||||
|
{
|
||||||
|
field: 'text',
|
||||||
|
component: 'Input',
|
||||||
|
label: '内容',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入内容'
|
||||||
|
},
|
||||||
|
colProps: {
|
||||||
|
span: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const columns = ref<TableColumn[]>([
|
||||||
|
{
|
||||||
|
label: '用户名',
|
||||||
|
field: 'username',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '窗口标题',
|
||||||
|
field: 'title',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '对话次数',
|
||||||
|
field: 'chatTotal',
|
||||||
|
align: 'center',
|
||||||
|
width: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Token消耗量',
|
||||||
|
field: 'tokenUsed',
|
||||||
|
align: 'center',
|
||||||
|
width: 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '最后一次对话时间',
|
||||||
|
field: 'endTime',
|
||||||
|
align: 'center',
|
||||||
|
width: 180,
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return dayjs(row.endTime).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '创建时间',
|
||||||
|
field: 'createTime',
|
||||||
|
width: 180,
|
||||||
|
align: 'center',
|
||||||
|
formatter: (row: any) => {
|
||||||
|
return dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const { register, tableObject, methods } = useTable({
|
||||||
|
getListApi: ConversationApi.page,
|
||||||
|
defaultParams: searchParams.value,
|
||||||
|
delListApi: ConversationApi.del
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearch = (values: any) => {
|
||||||
|
methods.setSearchParams(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagination = computed(() => {
|
||||||
|
return {
|
||||||
|
total: tableObject.total,
|
||||||
|
pageSize: tableObject.pageSize,
|
||||||
|
currentPage: tableObject.currentPage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleShowInfo = (row: any) => {
|
||||||
|
infoRef.value?.show(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
methods.getList()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema,
|
||||||
|
columns,
|
||||||
|
register,
|
||||||
|
handleSearch,
|
||||||
|
methods,
|
||||||
|
tableObject,
|
||||||
|
pagination,
|
||||||
|
handleShowInfo,
|
||||||
|
infoRef
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Delete, View } from '@element-plus/icons-vue'
|
||||||
|
import { Table } from '@/components/Table'
|
||||||
|
import useConversation from './composables'
|
||||||
|
import InfoList from './components/InfoList.vue'
|
||||||
|
import { searchSchemas } from './columns'
|
||||||
|
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
register,
|
||||||
|
handleSearch,
|
||||||
|
methods,
|
||||||
|
tableObject,
|
||||||
|
pagination,
|
||||||
|
handleShowInfo,
|
||||||
|
infoRef
|
||||||
|
} = useConversation()
|
||||||
|
|
||||||
|
const actionColumn = {
|
||||||
|
label: '操作',
|
||||||
|
field: 'action',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-2">
|
||||||
|
<Search :schema="searchSchemas" inline @search="handleSearch" />
|
||||||
|
|
||||||
|
<Table :columns="[...columns, actionColumn]" :data="tableObject.tableList" :pagination="pagination" @register="register">
|
||||||
|
<template #action="{ row }">
|
||||||
|
<el-button :icon="View" text type="primary" @click="handleShowInfo(row)" />
|
||||||
|
<el-button :icon="Delete" text type="danger" @click="methods.delList(row.id, false)" />
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<InfoList ref="infoRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,57 @@
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Delete } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
|
import { Table } from '@/components/Table'
|
||||||
|
import useMessage from './composables'
|
||||||
|
import ConversationList from './conversation/index.vue'
|
||||||
|
import { searchSchemas } from './columns'
|
||||||
|
import { format } from 'path'
|
||||||
|
const activeName = ref('1')
|
||||||
|
const {
|
||||||
|
schema,
|
||||||
|
columns,
|
||||||
|
register,
|
||||||
|
handleSearch,
|
||||||
|
methods,
|
||||||
|
tableObject,
|
||||||
|
pagination,
|
||||||
|
handleDel
|
||||||
|
} = useMessage()
|
||||||
|
|
||||||
|
const actionColumn = {
|
||||||
|
label: '操作',
|
||||||
|
field: 'action',
|
||||||
|
width: 70,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<el-tabs v-model="activeName">
|
||||||
|
<el-tab-pane label="会话消息列表" name="1">
|
||||||
|
<div class="mt-2">
|
||||||
|
<Search :schema="searchSchemas" @search="handleSearch"/>
|
||||||
|
<Table
|
||||||
|
:columns="[...columns, actionColumn]"
|
||||||
|
:data="tableObject.tableList"
|
||||||
|
:pagination="pagination"
|
||||||
|
@register="register"
|
||||||
|
>
|
||||||
|
<template #action="{row}">
|
||||||
|
<el-button :icon="Delete" text type="danger" @click="methods.delList(row.id, false)" />
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="会话窗口列表" name="2">
|
||||||
|
<ConversationList />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,89 @@
|
||||||
|
import {ProviderEnum} from './provider';
|
||||||
|
|
||||||
|
export const baseColumns = [
|
||||||
|
{
|
||||||
|
label: '模型别名',
|
||||||
|
field: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '模型版本',
|
||||||
|
field: 'model',
|
||||||
|
width: '140',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '回复上限',
|
||||||
|
field: 'responseLimit',
|
||||||
|
width: '100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '生成随机性',
|
||||||
|
field: 'temperature',
|
||||||
|
width: '100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Top P',
|
||||||
|
field: 'topP',
|
||||||
|
width: '100',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const openaiColumns = [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
label: 'Api Key',
|
||||||
|
field: 'apiKey',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ollamaColumns = [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
label: 'Base Url',
|
||||||
|
field: 'baseUrl',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const qfanColumns = [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
label: 'Api Key',
|
||||||
|
field: 'apiKey',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const qwenColumns = [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
label: 'Api Key',
|
||||||
|
field: 'apiKey',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const zhipuColumns = [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
label: 'Api Key',
|
||||||
|
field: 'apiKey',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getColumns(provider: string) {
|
||||||
|
switch (provider) {
|
||||||
|
case ProviderEnum.OLLAMA: {
|
||||||
|
return ollamaColumns;
|
||||||
|
}
|
||||||
|
case ProviderEnum.Q_FAN: {
|
||||||
|
return qfanColumns;
|
||||||
|
}
|
||||||
|
case ProviderEnum.Q_WEN: {
|
||||||
|
return qwenColumns;
|
||||||
|
}
|
||||||
|
case ProviderEnum.ZHIPU: {
|
||||||
|
return zhipuColumns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...openaiColumns, {
|
||||||
|
label: '操作',
|
||||||
|
field: 'action',
|
||||||
|
width: '150',
|
||||||
|
}];
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
/*
|
||||||
|
* 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 { ProviderEnum } from './provider';
|
||||||
|
|
||||||
|
export const LLMProviders: any[] = [
|
||||||
|
{
|
||||||
|
model: ProviderEnum.OPENAI,
|
||||||
|
name: 'OpenAI',
|
||||||
|
models: ['gpt-3.5-turbo', 'gpt-4', 'gpt-4-32k', 'gpt-4-turbo', 'gpt-4o'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.Q_FAN,
|
||||||
|
name: '百度千帆',
|
||||||
|
models: [
|
||||||
|
'ERNIE-Bot',
|
||||||
|
'ERNIE-Bot 4.0',
|
||||||
|
'ERNIE-Bot-8K',
|
||||||
|
'ERNIE-Bot-turbo',
|
||||||
|
'ERNIE-Speed-128K',
|
||||||
|
'EB-turbo-AppBuilder',
|
||||||
|
'Yi-34B-Chat',
|
||||||
|
'BLOOMZ-7B',
|
||||||
|
'Qianfan-BLOOMZ-7B-compressed',
|
||||||
|
'Mixtral-8x7B-Instruct',
|
||||||
|
'Llama-2-7b-chat',
|
||||||
|
'Llama-2-13b-chat',
|
||||||
|
'Llama-2-70b-chat',
|
||||||
|
'Qianfan-Chinese-Llama-2-7B',
|
||||||
|
'ChatGLM2-6B-32K',
|
||||||
|
'AquilaChat-7B',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.Q_WEN,
|
||||||
|
name: '阿里百炼',
|
||||||
|
models: [
|
||||||
|
'qwen-turbo',
|
||||||
|
'qwen-plus',
|
||||||
|
'qwen-max',
|
||||||
|
'qwen-max-longcontext',
|
||||||
|
'qwen-7b-chat',
|
||||||
|
'qwen-14b-chat',
|
||||||
|
'qwen-72b-chat',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.ZHIPU,
|
||||||
|
name: '智谱清言',
|
||||||
|
models: [
|
||||||
|
'glm-4',
|
||||||
|
'glm-4v',
|
||||||
|
'glm-4-air',
|
||||||
|
'glm-4-airx',
|
||||||
|
'glm-4-flash',
|
||||||
|
'glm-3-turbo',
|
||||||
|
'chatglm_turbo',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.GITEEAI,
|
||||||
|
name: 'Gitee AI',
|
||||||
|
models: [
|
||||||
|
'Qwen2-72B-Instruct',
|
||||||
|
'Qwen2-7B-Instruct',
|
||||||
|
'Qwen2.5-72B-Instruct',
|
||||||
|
'glm-4-9b-chat',
|
||||||
|
'deepseek-coder-33B-instruct',
|
||||||
|
'codegeex4-all-9b',
|
||||||
|
'Yi-34B-Chat',
|
||||||
|
'code-raccoon-v1',
|
||||||
|
'Qwen2.5-Coder-32B-Instruct',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.DEEPSEEK,
|
||||||
|
name: 'DeepSeek',
|
||||||
|
models: ['deepseek-chat', 'deepseek-coder'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.DOUYIN,
|
||||||
|
name: '抖音豆包',
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.SILICON,
|
||||||
|
name: '硅基流动',
|
||||||
|
models: [
|
||||||
|
'deepseek-ai/DeepSeek-V2-Chat',
|
||||||
|
'deepseek-ai/DeepSeek-Coder-V2-Instruct',
|
||||||
|
'deepseek-ai/DeepSeek-V2.5',
|
||||||
|
'Qwen/Qwen2.5-72B-Instruct-128K',
|
||||||
|
'Qwen/Qwen2.5-72B-Instruct',
|
||||||
|
'Qwen/Qwen2-VL-72B-Instruct',
|
||||||
|
'Qwen/Qwen2.5-32B-Instruct',
|
||||||
|
'Qwen/Qwen2.5-14B-Instruct',
|
||||||
|
'Qwen/Qwen2.5-7B-Instruct',
|
||||||
|
'Qwen/Qwen2.5-Math-72B-Instruct',
|
||||||
|
'Qwen/Qwen2.5-Coder-7B-Instruct',
|
||||||
|
'Qwen/Qwen2-72B-Instruct',
|
||||||
|
'Qwen/Qwen2-7B-Instruct',
|
||||||
|
'Qwen/Qwen2-1.5B-Instruct',
|
||||||
|
'Qwen/Qwen2-57B-A14B-Instruct',
|
||||||
|
'TeleAI/TeleChat2',
|
||||||
|
'01-ai/Yi-1.5-34B-Chat-16K',
|
||||||
|
'01-ai/Yi-1.5-9B-Chat-16K',
|
||||||
|
'01-ai/Yi-1.5-6B-Chat',
|
||||||
|
'THUDM/chatglm3-6b',
|
||||||
|
'THUDM/glm-4-9b-chat',
|
||||||
|
'Vendor-A/Qwen/Qwen2-72B-Instruct',
|
||||||
|
'Vendor-A/Qwen/Qwen2.5-72B-Instruct',
|
||||||
|
'internlm/internlm2_5-7b-chat',
|
||||||
|
'internlm/internlm2_5-20b-chat',
|
||||||
|
'OpenGVLab/InternVL2-Llama3-76B',
|
||||||
|
'OpenGVLab/InternVL2-26B',
|
||||||
|
'nvidia/Llama-3.1-Nemotron-70B-Instruct',
|
||||||
|
'meta-llama/Meta-Llama-3.1-405B-Instruct',
|
||||||
|
'meta-llama/Meta-Llama-3.1-70B-Instruct',
|
||||||
|
'meta-llama/Meta-Llama-3.1-8B-Instruct',
|
||||||
|
'meta-llama/Meta-Llama-3-8B-Instruct',
|
||||||
|
'meta-llama/Meta-Llama-3-70B-Instruct',
|
||||||
|
'google/gemma-2-27b-it',
|
||||||
|
'google/gemma-2-9b-it',
|
||||||
|
'Pro/Qwen/Qwen2.5-7B-Instruct',
|
||||||
|
'Pro/Qwen/Qwen2-7B-Instruct',
|
||||||
|
'Pro/Qwen/Qwen2-1.5B-Instruct',
|
||||||
|
'Pro/Qwen/Qwen2-VL-7B-Instruct',
|
||||||
|
'Pro/01-ai/Yi-1.5-9B-Chat-16K',
|
||||||
|
'Pro/01-ai/Yi-1.5-6B-Chat',
|
||||||
|
'Pro/THUDM/chatglm3-6b',
|
||||||
|
'Pro/THUDM/glm-4-9b-chat',
|
||||||
|
'Pro/internlm/internlm2_5-7b-chat',
|
||||||
|
'Pro/OpenGVLab/InternVL2-8B',
|
||||||
|
'Pro/meta-llama/Meta-Llama-3-8B-Instruct',
|
||||||
|
'Pro/meta-llama/Meta-Llama-3.1-8B-Instruct',
|
||||||
|
'Pro/google/gemma-2-9b-it',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.YI,
|
||||||
|
name: '零一万物',
|
||||||
|
models: [
|
||||||
|
'yi-lightning',
|
||||||
|
'yi-large',
|
||||||
|
'yi-medium',
|
||||||
|
'yi-medium-200k',
|
||||||
|
'yi-spark',
|
||||||
|
'yi-large-rag',
|
||||||
|
'yi-large-turbo',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.SPARK,
|
||||||
|
name: '讯飞星火',
|
||||||
|
// models: ['lite', 'generalv3', 'pro-128k', 'generalv3.5', 'max-32k', '4.0Ultra'],
|
||||||
|
models: [
|
||||||
|
{ label: 'Spark Lite', value: 'lite' },
|
||||||
|
{ label: 'Spark Pro', value: 'generalv3' },
|
||||||
|
{ label: 'Spark Pro-128K', value: 'pro-128k' },
|
||||||
|
{ label: 'Spark Max', value: 'generalv3.5' },
|
||||||
|
{ label: 'Spark Max-32K', value: 'max-32k' },
|
||||||
|
{ label: 'Spark4.0 Ultra', value: '4.0Ultra' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.OLLAMA,
|
||||||
|
name: 'Ollama',
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.AZURE_OPENAI,
|
||||||
|
name: 'Azure OpenAI',
|
||||||
|
models: ['gpt-3.5-turbo', 'gpt-4', 'gpt-4-32k', 'gpt-4-turbo', 'gpt-4o'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.GEMINI,
|
||||||
|
name: 'Gemini',
|
||||||
|
models: ['gemini-1.5-pro'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.CLAUDE,
|
||||||
|
name: 'Claude',
|
||||||
|
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',
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
export enum ProviderEnum {
|
||||||
|
OPENAI = 'OPENAI',
|
||||||
|
AZURE_OPENAI = 'AZURE_OPENAI',
|
||||||
|
GEMINI = 'GEMINI',
|
||||||
|
OLLAMA = 'OLLAMA',
|
||||||
|
CLAUDE = 'CLAUDE',
|
||||||
|
Q_FAN = 'Q_FAN',
|
||||||
|
Q_WEN = 'Q_WEN',
|
||||||
|
ZHIPU = 'ZHIPU',
|
||||||
|
GITEEAI = 'GITEEAI',
|
||||||
|
DEEPSEEK = 'DEEPSEEK',
|
||||||
|
DOUYIN = 'DOUYIN',
|
||||||
|
SILICON = 'SILICON',
|
||||||
|
YI = 'YI',
|
||||||
|
SPARK = 'SPARK',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModels(provider: string, providers: Array<any>) {
|
||||||
|
const arr = providers.filter((i) => i.model === provider);
|
||||||
|
if (arr.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (typeof arr[0].models[0] === 'string') {
|
||||||
|
return arr[0].models.map((i) => {
|
||||||
|
return {
|
||||||
|
label: i,
|
||||||
|
value: i,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return arr[0].models;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTitle(provider: string, providers: Array<any>) {
|
||||||
|
return providers.filter((i) => i.model === provider)[0].name;
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
/*
|
||||||
|
* 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 { FormSchema } from '@/components/Form';
|
||||||
|
import { LLMProviders } from './consts';
|
||||||
|
import { getModels, ProviderEnum } from './provider';
|
||||||
|
// import { ModelTypeEnum } from '@/api/models';
|
||||||
|
import {FormSchema} from "@/types/form";
|
||||||
|
import { isNullOrWhitespace } from '@/utils/is';
|
||||||
|
|
||||||
|
const baseSchemas: FormSchema[] = [
|
||||||
|
// {
|
||||||
|
// field: 'id',
|
||||||
|
// label: 'ID',
|
||||||
|
// component: 'Input',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// field: 'type',
|
||||||
|
// label: 'type',
|
||||||
|
// component: 'Input',
|
||||||
|
// // value: ModelTypeEnum.CHAT,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
field: 'provider',
|
||||||
|
label: 'LLM供应商',
|
||||||
|
component: 'Select',
|
||||||
|
// isHidden: true,
|
||||||
|
componentProps: {
|
||||||
|
placeholder: 'LLM供应商',
|
||||||
|
options: LLMProviders,
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'model',
|
||||||
|
},
|
||||||
|
// rules: [{ required: true, message: '请选择LLM供应商', trigger: ['blur'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
label: '模型别名',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请输入模型别名', trigger: ['blur'] }]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入模型别名',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiKey',
|
||||||
|
label: 'Api Key',
|
||||||
|
labelMessage: '模型的ApiKey',
|
||||||
|
component: 'Input',
|
||||||
|
// rules: [{ required: true, message: '请输入API Key', trigger: ['blur'] }],
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入Api Key',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'responseLimit',
|
||||||
|
label: '回复上限',
|
||||||
|
labelMessage: '控制模型输出的Tokens长度上限。通常 100 Tokens 约等于150个中文汉字',
|
||||||
|
component: 'Slider',
|
||||||
|
formItemProps:{
|
||||||
|
rules: [{ type: 'number', required: true, message: '请输入回复上限', trigger: ['blur'] }]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
showTooltip: true,
|
||||||
|
value: 2000,
|
||||||
|
step: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 8192,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'temperature',
|
||||||
|
label: '生成随机性',
|
||||||
|
labelMessage: '调高参数会使得模型的输出更多样性和创新性,反之降低参数将会减少多样性',
|
||||||
|
component: 'Slider',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ type: 'number', required: true, message: '请输入生成随机性', trigger: ['blur'] }]
|
||||||
|
},
|
||||||
|
value:0.2,
|
||||||
|
componentProps: {
|
||||||
|
showTooltip: true,
|
||||||
|
step: 0.05,
|
||||||
|
min: 0,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'topP',
|
||||||
|
label: 'Top P',
|
||||||
|
labelMessage:
|
||||||
|
'模型在生成输出时会从概率最高的词汇开始选择,直到这些词汇的总概率累积达到Top p值。这样可以限制模型只选择这些高概率的词汇,从而控制输出内容的多样性。建议不要与“生成随机性“同时调整',
|
||||||
|
component: 'Slider',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ type: 'number', required: true, message: '请输入', trigger: ['blur'] }]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
showTooltip: true,
|
||||||
|
value: 0.8,
|
||||||
|
step: 0.1,
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getSchemas(provider: string) {
|
||||||
|
const list = JSON.parse(JSON.stringify(baseSchemas));
|
||||||
|
console.log(provider);
|
||||||
|
const modelSchema: any = {
|
||||||
|
field: 'model',
|
||||||
|
label: '模型版本',
|
||||||
|
labelMessage: '该LLM供应商对应的模型版本号',
|
||||||
|
component: 'Select',
|
||||||
|
rules: [{ required: true, message: '请选择模型', trigger: ['blur'] }],
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择模型版本(可以手动输入)',
|
||||||
|
filterable: true,
|
||||||
|
tag: true,
|
||||||
|
options: getModels(provider, LLMProviders),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
list.splice(1, 0, modelSchema);
|
||||||
|
|
||||||
|
let value: any = undefined;
|
||||||
|
let labelMessage: any = '模型的基础请求URL地址(或中转地址)';
|
||||||
|
let disabled = false;
|
||||||
|
switch (provider) {
|
||||||
|
case ProviderEnum.GITEEAI:
|
||||||
|
disabled = true;
|
||||||
|
value = 'https://ai.gitee.com/v1';
|
||||||
|
labelMessage = '对于Gitee AI,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
case ProviderEnum.DEEPSEEK:
|
||||||
|
disabled = true;
|
||||||
|
value = 'https://api.deepseek.com/v1';
|
||||||
|
labelMessage = '对于DeepSeek模型,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
case ProviderEnum.SILICON:
|
||||||
|
disabled = true;
|
||||||
|
value = 'https://api.siliconflow.cn/v1';
|
||||||
|
labelMessage = '对于硅基流动模型,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
case ProviderEnum.DOUYIN:
|
||||||
|
disabled = true;
|
||||||
|
value = 'https://ark.cn-beijing.volces.com/api/v3';
|
||||||
|
labelMessage = '对于抖音豆包模型,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
case ProviderEnum.YI:
|
||||||
|
disabled = true;
|
||||||
|
value = 'https://api.lingyiwanwu.com/v1';
|
||||||
|
labelMessage = '对于零一模型,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
case ProviderEnum.SPARK:
|
||||||
|
disabled = true;
|
||||||
|
value = 'https://spark-api-open.xf-yun.com/v1';
|
||||||
|
labelMessage = '对于讯飞星火大模型,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const baseUlrSchema: any = {
|
||||||
|
field: 'baseUrl',
|
||||||
|
label: 'Base Url',
|
||||||
|
labelMessage,
|
||||||
|
component: 'Input',
|
||||||
|
value,
|
||||||
|
componentProps: {
|
||||||
|
disabled,
|
||||||
|
placeholder: '请输入BaseUrl',
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
trigger: ['blur'],
|
||||||
|
validator: (_, value: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const urlRegex =
|
||||||
|
/^(https?:\/\/)?((([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|localhost|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(\/.*)?)$/;
|
||||||
|
if (isNullOrWhitespace(value) || urlRegex.test(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return new Error('URL格式错误');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
list.splice(3, 0, baseUlrSchema);
|
||||||
|
return list;
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {Form} from '@/components/Form'
|
||||||
|
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
|
||||||
|
schemas.value = []
|
||||||
|
nextTick(() => {
|
||||||
|
formRef.value?.clearForm()
|
||||||
|
formRef.value?.setSchema(schemas.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = async (data: any = {}) => {
|
||||||
|
visible.value = true
|
||||||
|
isEdit.value = !!data.id
|
||||||
|
await nextTick()
|
||||||
|
formRef.value.setValues(data)
|
||||||
|
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
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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 :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,185 +1,149 @@
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
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, 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 {getModels, ProviderEnum} from './composables/provider';
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
provider: ProviderEnum.OPENAI
|
||||||
|
});
|
||||||
|
const message = ElMessage;
|
||||||
|
const dialog = ElMessageBox;
|
||||||
|
const actionRef = ref();
|
||||||
|
const editRef = ref();
|
||||||
|
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('获取模型列表失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 provider 变化重新加载数据
|
||||||
|
watch(() => formData.value.provider, () => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
onMounted(() => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = computed(() => {
|
||||||
|
nextTick();
|
||||||
|
return getColumns(formData.value.provider);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function addModel() {
|
||||||
|
console.log(formData.value.provider);
|
||||||
|
editRef.value.show({provider: formData.value.provider, type: ModelTypeEnum.CHAT });
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<div class="flex children">
|
||||||
<el-form
|
<el-scrollbar class="h-full w-300px pl-10px pr-20px">
|
||||||
class="-mb-15px"
|
<div
|
||||||
:model="queryParams"
|
v-for="(item,index) in LLMProviders" :key="index"
|
||||||
ref="queryFormRef"
|
:class="{active: formData.provider === item.model}" class="menu"
|
||||||
:inline="true"
|
@click="formData.provider = item.model">
|
||||||
label-width="68px"
|
<span>{{ item.name }}</span>
|
||||||
>
|
</div>
|
||||||
<el-form-item label="模型名字" prop="name">
|
</el-scrollbar>
|
||||||
<el-input
|
<div class="h-full flex-1 px-20px" >
|
||||||
v-model="queryParams.name"
|
<el-alert
|
||||||
placeholder="请输入模型名字"
|
class="w-full mb-10px min-alert"
|
||||||
clearable
|
title="对于完全适配OpenAI接口格式的模型都可在OpenAI中配置(只需要定义BaseUrl)"
|
||||||
@keyup.enter="handleQuery"
|
type="warning"
|
||||||
class="!w-240px"
|
show-icon
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
<el-button :icon="Plus" class="my-10px" type="primary" @click="addModel">新增模型</el-button>
|
||||||
<el-form-item label="模型标识" prop="model">
|
<Table :loading="loading" class="table-wrapper" height="100%" border :columns="columns" :data="tableData" >
|
||||||
<el-input
|
<template #action="{row}">
|
||||||
v-model="queryParams.model"
|
<el-button text :icon="Edit" @click="handleEdit(row)"/>
|
||||||
placeholder="请输入模型标识"
|
<el-button text :icon="Delete" type="danger" @click="handleDel(row)"/>
|
||||||
clearable
|
</template>
|
||||||
@keyup.enter="handleQuery"
|
</Table>
|
||||||
class="!w-240px"
|
<editCom ref="editRef" @reload="reloadTable"/>
|
||||||
/>
|
</div>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item label="模型平台" prop="platform">
|
|
||||||
<el-input
|
|
||||||
v-model="queryParams.platform"
|
|
||||||
placeholder="请输入模型平台"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="handleQuery"
|
|
||||||
class="!w-240px"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
|
||||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
@click="openForm('create')"
|
|
||||||
v-hasPermi="['ai:chat-model:create']"
|
|
||||||
>
|
|
||||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
|
|
||||||
<!-- 列表 -->
|
|
||||||
<ContentWrap>
|
|
||||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
|
||||||
<el-table-column label="所属平台" align="center" prop="platform">
|
|
||||||
<template #default="scope">
|
|
||||||
<dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="模型名字" align="center" prop="name" />
|
|
||||||
<el-table-column label="模型标识" align="center" prop="model" />
|
|
||||||
<el-table-column label="API 秘钥" align="center" prop="keyId" min-width="140">
|
|
||||||
<template #default="scope">
|
|
||||||
<span>{{ apiKeyList.find((item) => item.id === scope.row.keyId)?.name }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="排序" align="center" prop="sort" />
|
|
||||||
<el-table-column label="状态" align="center" prop="status">
|
|
||||||
<template #default="scope">
|
|
||||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="温度参数" align="center" prop="temperature" />
|
|
||||||
<el-table-column label="回复数 Token 数" align="center" prop="maxTokens" min-width="140" />
|
|
||||||
<el-table-column label="上下文数量" align="center" prop="maxContexts" />
|
|
||||||
<el-table-column label="操作" align="center">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-button
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
@click="openForm('update', scope.row.id)"
|
|
||||||
v-hasPermi="['ai:chat-model:update']"
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
link
|
|
||||||
type="danger"
|
|
||||||
@click="handleDelete(scope.row.id)"
|
|
||||||
v-hasPermi="['ai:chat-model:delete']"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<!-- 分页 -->
|
|
||||||
<Pagination
|
|
||||||
:total="total"
|
|
||||||
v-model:page="queryParams.pageNo"
|
|
||||||
v-model:limit="queryParams.pageSize"
|
|
||||||
@pagination="getList"
|
|
||||||
/>
|
|
||||||
</ContentWrap>
|
|
||||||
|
|
||||||
<!-- 表单弹窗:添加/修改 -->
|
|
||||||
<ChatModelForm ref="formRef" @success="getList" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<style lang="scss" scoped>
|
||||||
import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
|
.children {
|
||||||
import ChatModelForm from './ChatModelForm.vue'
|
height: calc(100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - (var(--app-content-padding) * 3)) !important;
|
||||||
import { DICT_TYPE } from '@/utils/dict'
|
box-sizing: border-box;
|
||||||
import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
|
||||||
|
|
||||||
/** API 聊天模型 列表 */
|
& > div:nth-child(2) {
|
||||||
defineOptions({ name: 'AiChatModel' })
|
width: calc(100% - 300px);
|
||||||
|
|
||||||
const message = useMessage() // 消息弹窗
|
|
||||||
const { t } = useI18n() // 国际化
|
|
||||||
|
|
||||||
const loading = ref(true) // 列表的加载中
|
|
||||||
const list = ref<ChatModelVO[]>([]) // 列表的数据
|
|
||||||
const total = ref(0) // 列表的总页数
|
|
||||||
const queryParams = reactive({
|
|
||||||
pageNo: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
name: undefined,
|
|
||||||
model: undefined,
|
|
||||||
platform: undefined
|
|
||||||
})
|
|
||||||
const queryFormRef = ref() // 搜索的表单
|
|
||||||
const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表
|
|
||||||
|
|
||||||
/** 查询列表 */
|
|
||||||
const getList = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = await ChatModelApi.getChatModelPage(queryParams)
|
|
||||||
list.value = data.list
|
|
||||||
total.value = data.total
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 搜索按钮操作 */
|
.table-wrapper {
|
||||||
const handleQuery = () => {
|
height: calc(100% - 100px);
|
||||||
queryParams.pageNo = 1
|
|
||||||
getList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置按钮操作 */
|
.menu {
|
||||||
const resetQuery = () => {
|
transition: all .15s;
|
||||||
queryFormRef.value.resetFields()
|
cursor: pointer;
|
||||||
handleQuery()
|
padding: 12px 10px;
|
||||||
}
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
&.active {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
/** 添加/修改操作 */
|
&:hover {
|
||||||
const formRef = ref()
|
&:not(&.active) {
|
||||||
const openForm = (type: string, id?: number) => {
|
background-color: var(--el-color-info-light-7);
|
||||||
formRef.value.open(type, id)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
/** 删除按钮操作 */
|
|
||||||
const handleDelete = async (id: number) => {
|
|
||||||
try {
|
|
||||||
// 删除的二次确认
|
|
||||||
await message.delConfirm()
|
|
||||||
// 发起删除
|
|
||||||
await ChatModelApi.deleteChatModel(id)
|
|
||||||
message.success(t('common.delSuccess'))
|
|
||||||
// 刷新列表
|
|
||||||
await getList()
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化 **/
|
|
||||||
onMounted(async () => {
|
|
||||||
getList()
|
|
||||||
// 获得下拉数据
|
|
||||||
apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {FormSchema} from "@/types/form";
|
||||||
|
import {ElTag} from "element-plus";
|
||||||
|
import {EmbedStoreApi} from "@/api/new-ai/embed-store";
|
||||||
|
import { useTable } from "@/hooks/web/useTable";
|
||||||
|
import { TableColumn } from "@/types/table";
|
||||||
|
|
||||||
|
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 editRef = ref()
|
||||||
|
const searchParams = ref({});
|
||||||
|
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) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return arr[0].label;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shema = ref<FormSchema[]>([
|
||||||
|
{
|
||||||
|
label: '数据库别名',
|
||||||
|
field: 'name',
|
||||||
|
component: 'Input',
|
||||||
|
colProps: {
|
||||||
|
span: 6
|
||||||
|
},
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const columns = ref<TableColumn[]>([
|
||||||
|
{
|
||||||
|
label: '数据库别名',
|
||||||
|
field: 'name',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '供应商',
|
||||||
|
field: 'provider',
|
||||||
|
align: 'center',
|
||||||
|
width: '120',
|
||||||
|
render(row) {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => getProviderLabel(row.provider),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '向量纬度',
|
||||||
|
field: 'dimension',
|
||||||
|
align: 'center',
|
||||||
|
width: '100',
|
||||||
|
formatter(row) {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{
|
||||||
|
size: 'small',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => row.dimension,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据库地址',
|
||||||
|
field: 'host',
|
||||||
|
align: 'center',
|
||||||
|
width: '110',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据库端口',
|
||||||
|
field: 'port',
|
||||||
|
align: 'center',
|
||||||
|
width: '100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据库用户名',
|
||||||
|
field: 'username',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据库密码',
|
||||||
|
field: 'password',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据库名',
|
||||||
|
field: 'databaseName',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '表名称',
|
||||||
|
field: 'tableName',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '操作',
|
||||||
|
field: "action",
|
||||||
|
width: 150
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {register, tableObject, methods} = useTable({
|
||||||
|
getListApi: EmbedStoreApi.getEmbedStorePage,
|
||||||
|
defaultParams: searchParams.value,
|
||||||
|
delListApi: EmbedStoreApi.deleteEmbedStore
|
||||||
|
});
|
||||||
|
|
||||||
|
// 打开编辑对话框
|
||||||
|
const open = (data?: any) => {
|
||||||
|
editRef.value.show(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = (values: any) => {
|
||||||
|
methods.setSearchParams(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagination = computed(() => {
|
||||||
|
return {
|
||||||
|
total: tableObject.total,
|
||||||
|
pageSize: tableObject.pageSize,
|
||||||
|
currentPage: tableObject.currentPage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleDel = async (id: string | number) => {
|
||||||
|
try {
|
||||||
|
await methods.delList(id, false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete embed store:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shema,
|
||||||
|
columns,
|
||||||
|
register,
|
||||||
|
open,
|
||||||
|
handleSearch,
|
||||||
|
ProviderConst,
|
||||||
|
getProviderLabel,
|
||||||
|
methods,
|
||||||
|
tableObject,
|
||||||
|
pagination,
|
||||||
|
handleDel,
|
||||||
|
editRef
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 loading = 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: 'databaseName',
|
||||||
|
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" :close-on-click-modal="!loading" :close-on-press-escape="!loading" draggable :title="isEdit ? '编辑向量数据库' : '新增向量数据库'" width="800px" @close="close">
|
||||||
|
<Form ref="formRef" :model="formData" :schema="schemas" />
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">确认</el-button>
|
||||||
|
<el-button @click="close" :loading="loading">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
</style>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {Plus, Edit, Delete} from "@element-plus/icons-vue";
|
||||||
|
import {Table} from "@/components/Table";
|
||||||
|
import {Search} from "@/components/Search";
|
||||||
|
import useEmbedStore from './composables'
|
||||||
|
import EditCom from './edit.vue'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const { shema, columns, register, open, handleSearch,methods, tableObject, pagination, handleDel, editRef } = useEmbedStore()
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
methods?.getList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<content-wrap>
|
||||||
|
<Search :schema="shema" @search="handleSearch" @reset="handleSearch"/>
|
||||||
|
</content-wrap>
|
||||||
|
<ContentWrap v-loading="tableObject.loading">
|
||||||
|
<el-alert
|
||||||
|
class="w-full mb-10px min-alert"
|
||||||
|
title="注意:请慎重修改模型的向量纬度参数(Dimension),此参数需要和向量库匹配(错误修改可能将影响已有的向量数据)"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-button class="my-10px" type="primary" :icon="Plus" @click="open()">新增向量数据库</el-button>
|
||||||
|
<Table height="calc(100% - 400px)" border :data="tableObject.tableList" :columns="columns" :pagination="pagination" @register="register">
|
||||||
|
<template #action="{ row }">
|
||||||
|
<el-button type="primary" text :icon="Edit" @click="open(row)" />
|
||||||
|
<el-button type="danger" text :icon="Delete" @click="handleDel(row.id)" />
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<EditCom ref="editRef" @reload="methods?.getList" />
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,34 @@
|
||||||
|
import {ProviderEnum} from "@/views/ai/model/chatModel/composables/provider";
|
||||||
|
|
||||||
|
export const LLMProviders: any[] = [
|
||||||
|
{
|
||||||
|
model: ProviderEnum.OPENAI,
|
||||||
|
name: 'OpenAI',
|
||||||
|
models: ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.Q_FAN,
|
||||||
|
name: '百度千帆',
|
||||||
|
models: ['bge-large-zh', 'bge-large-en', 'tao-8k'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.Q_WEN,
|
||||||
|
name: '阿里百炼',
|
||||||
|
models: ['text-embedding-v3'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.ZHIPU,
|
||||||
|
name: '智谱清言',
|
||||||
|
models: ['embedding-2', 'embedding-3'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.DOUYIN,
|
||||||
|
name: '抖音豆包',
|
||||||
|
models: ['text-240715', 'text-240515'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.OLLAMA,
|
||||||
|
name: 'Ollama',
|
||||||
|
models: ['text2vec-bge-large-chinese:latest'],
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,107 @@
|
||||||
|
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 editRef = ref()
|
||||||
|
const formData = ref({
|
||||||
|
provider: ProviderEnum.OPENAI,
|
||||||
|
type: ModelTypeEnum.EMBEDDING
|
||||||
|
});
|
||||||
|
const tableData = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const baseColumns = [
|
||||||
|
{
|
||||||
|
label: '模型别名',
|
||||||
|
field: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '模型版本',
|
||||||
|
field: 'model',
|
||||||
|
width: '160',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '向量纬度',
|
||||||
|
field: 'dimension',
|
||||||
|
align: 'center',
|
||||||
|
width: '100',
|
||||||
|
render(row) {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{
|
||||||
|
size: 'small',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => row.dimension,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Api Key',
|
||||||
|
field: 'apiKey',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Base Url',
|
||||||
|
field: 'baseUrl',
|
||||||
|
}, {label: '操作', field: 'action', width: 150}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await ModelApi.getModelList({ provider: formData.value.provider, type: ModelTypeEnum.EMBEDDING }).finally(() => loading.value = false);
|
||||||
|
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, 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,
|
||||||
|
loadData,
|
||||||
|
handleDel,
|
||||||
|
handleEdit,
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
|
||||||
|
|
||||||
|
import {FormSchema} from "@/types/form";
|
||||||
|
// import { ModelTypeEnum } from '@/api/models';
|
||||||
|
import {getModels, ProviderEnum} from "@/views/ai/model/chatModel/composables/provider";
|
||||||
|
import {LLMProviders} from "@/views/ai/model/embedding/composables/consts";
|
||||||
|
// import { LLMProviders } from './consts';
|
||||||
|
const baseSchemas: FormSchema[] = [
|
||||||
|
// {
|
||||||
|
// field: 'id',
|
||||||
|
// label: 'ID',
|
||||||
|
// component: 'Input',
|
||||||
|
// isHidden: true,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// field: 'type',
|
||||||
|
// label: 'type',
|
||||||
|
// component: 'Input',
|
||||||
|
// isHidden: true,
|
||||||
|
// value: ModelTypeEnum.EMBEDDING,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
field: 'provider',
|
||||||
|
label: 'LLM供应商',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择LLM供应商',
|
||||||
|
options: LLMProviders,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
label: '模型别名',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请输入模型别名', trigger: ['blur'] }]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入模型别名',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'apiKey',
|
||||||
|
label: 'Api Key',
|
||||||
|
labelMessage: '模型的ApiKey',
|
||||||
|
component: 'Input',
|
||||||
|
// rules: [{ required: true, message: '请输入ApiKey', trigger: ['blur'] }],
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入ApiKey',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'dimension',
|
||||||
|
label: '向量纬度',
|
||||||
|
component: 'Select',
|
||||||
|
value: 1024,
|
||||||
|
labelMessage: '慎重修改此参数,纬度高会消耗更多的算力,但纬度高并不代表搜索更精确',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入向量纬度',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '512',
|
||||||
|
value: 512,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '768',
|
||||||
|
value: 768,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1024',
|
||||||
|
value: 1024,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1536',
|
||||||
|
value: 1536,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
formItemProps:{
|
||||||
|
rules: [{ type: 'number', required: true, message: '请输入向量纬度', trigger: ['blur'] }]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getSchemas(provider: string) {
|
||||||
|
const list = JSON.parse(JSON.stringify(baseSchemas));
|
||||||
|
|
||||||
|
const modelSchema: any = {
|
||||||
|
field: 'model',
|
||||||
|
label: '模型版本',
|
||||||
|
labelMessage: '该LLM供应商对应的模型版本号',
|
||||||
|
component: 'Select',
|
||||||
|
rules: [{ required: true, message: '请选择模型', trigger: ['blur'] }],
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请选择模型版本',
|
||||||
|
filterable: true,
|
||||||
|
tag: true,
|
||||||
|
options: getModels(provider, LLMProviders),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
list.splice(list.length, 0, modelSchema);
|
||||||
|
|
||||||
|
let value: any = undefined;
|
||||||
|
let labelMessage: any = '模型的基础请求URL地址(或中转地址)';
|
||||||
|
let disabled = false;
|
||||||
|
switch (provider) {
|
||||||
|
case ProviderEnum.DOUYIN:
|
||||||
|
disabled = true;
|
||||||
|
value = 'https://ark.cn-beijing.volces.com/api/v3';
|
||||||
|
labelMessage = '对于抖音豆包模型,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
case ProviderEnum.Q_FAN:
|
||||||
|
disabled = true;
|
||||||
|
labelMessage = '对于百度千帆模型,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
case ProviderEnum.Q_WEN:
|
||||||
|
disabled = true;
|
||||||
|
labelMessage = '对于阿里千问模型,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
case ProviderEnum.ZHIPU:
|
||||||
|
disabled = true;
|
||||||
|
labelMessage = '对于智谱清言模型,此Url固定不可修改';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const baseUlrSchema: any = {
|
||||||
|
field: 'baseUrl',
|
||||||
|
label: 'Base Url',
|
||||||
|
labelMessage,
|
||||||
|
component: 'Input',
|
||||||
|
value,
|
||||||
|
componentProps: {
|
||||||
|
disabled,
|
||||||
|
placeholder: '请输入BaseUrl',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
list.splice(list.length, 0, baseUlrSchema);
|
||||||
|
return list;
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {Form} from '@/components/Form'
|
||||||
|
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: any = {}) => {
|
||||||
|
visible.value = true
|
||||||
|
isEdit.value = !!data.id
|
||||||
|
await nextTick()
|
||||||
|
formRef.value.setValues(data)
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
</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>
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {Plus, Delete, Edit} from "@element-plus/icons-vue";
|
||||||
|
import usePage from './composables/index'
|
||||||
|
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, loadData, loading,handleEdit, handleDel} = usePage()
|
||||||
|
|
||||||
|
const handleReload = () => {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<div class="children flex">
|
||||||
|
<el-scrollbar class="h-full w-300px pl-10px pr-20px">
|
||||||
|
<div
|
||||||
|
v-for="(item,index) in LLMProviders" :key="index"
|
||||||
|
:class="{active: formData.provider === item.model}" class="menu"
|
||||||
|
@click="formData.provider = item.model">
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
<div class="h-full flex-1 px-20px">
|
||||||
|
<el-alert
|
||||||
|
class="w-full mb-10px min-alert"
|
||||||
|
title="注意:为了实现向量数据库的动态切换,这里Embedding供应商统一选择支持1024纬度的模型"
|
||||||
|
type="info"
|
||||||
|
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" :pagination="false" :loading="loading">
|
||||||
|
<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>
|
||||||
|
<EditCom ref="editRef" @reload="handleReload" />
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.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:nth-child(2) {
|
||||||
|
width: calc(100% - 300px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
transition: all .15s;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,22 @@
|
||||||
|
export enum ProviderEnum {
|
||||||
|
OPENAI = 'OPENAI',
|
||||||
|
AZURE_OPENAI = 'AZURE_OPENAI',
|
||||||
|
ZHIPU = 'ZHIPU',
|
||||||
|
}
|
||||||
|
export const LLMProviders: any[] = [
|
||||||
|
{
|
||||||
|
model: ProviderEnum.OPENAI,
|
||||||
|
name: 'OpenAI',
|
||||||
|
models: ['dall-e-2', 'dall-e-3'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.AZURE_OPENAI,
|
||||||
|
name: 'Azure OpenAI',
|
||||||
|
models: ['dall-e-2', 'dall-e-3'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: ProviderEnum.ZHIPU,
|
||||||
|
name: '智谱清言',
|
||||||
|
models: ['cogview-3'],
|
||||||
|
},
|
||||||
|
]
|
|
@ -0,0 +1,128 @@
|
||||||
|
import {computed, nextTick, onMounted, 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([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const baseColumns = [
|
||||||
|
{
|
||||||
|
label: '模型别名',
|
||||||
|
field: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '模型版本',
|
||||||
|
field: 'model',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const openaiColumns = [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
label: 'Api Key',
|
||||||
|
field: 'apiKey',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const azureOpenaiColumns = [
|
||||||
|
...baseColumns,
|
||||||
|
{
|
||||||
|
label: 'Api Key',
|
||||||
|
field: 'apiKey',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Endpoint',
|
||||||
|
field: 'endpoint',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Deployment Name',
|
||||||
|
field: 'azureDeploymentName',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const zhipuColumns = [...baseColumns];
|
||||||
|
|
||||||
|
function getColumns(provider: string) {
|
||||||
|
switch (provider) {
|
||||||
|
case ProviderEnum.OPENAI: {
|
||||||
|
return openaiColumns;
|
||||||
|
}
|
||||||
|
case ProviderEnum.AZURE_OPENAI: {
|
||||||
|
return azureOpenaiColumns;
|
||||||
|
}
|
||||||
|
case ProviderEnum.ZHIPU: {
|
||||||
|
return zhipuColumns;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return baseColumns;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = computed(() => {
|
||||||
|
nextTick();
|
||||||
|
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 }).finally(() => loading.value = false);
|
||||||
|
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, 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,
|
||||||
|
loadData,
|
||||||
|
handleDel,
|
||||||
|
handleEdit,
|
||||||
|
loading,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,294 @@
|
||||||
|
/*
|
||||||
|
* 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 {FormSchema} from '@/types/form';
|
||||||
|
// import { LLMProviders, ProviderEnum } from './data';
|
||||||
|
// import { ModelTypeEnum } from '@/api/models';
|
||||||
|
import {isNullOrWhitespace} from '@/utils/is';
|
||||||
|
import {LLMProviders, ProviderEnum} from "@/views/ai/model/image/composables/consts";
|
||||||
|
|
||||||
|
const baseHeadSchemas: FormSchema[] = [
|
||||||
|
// {
|
||||||
|
// field: 'id',
|
||||||
|
// label: 'ID',
|
||||||
|
// component: 'Input',
|
||||||
|
// isHidden: true,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// field: 'type',
|
||||||
|
// label: 'type',
|
||||||
|
// component: 'Input',
|
||||||
|
// isHidden: true,
|
||||||
|
// defaultValue: ModelTypeEnum.TEXT_IMAGE,
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
field: 'provider',
|
||||||
|
label: 'LLM供应商',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: LLMProviders,
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'model',
|
||||||
|
},
|
||||||
|
formItemProps: {rules: [{required: true, message: '请选择LLM供应商', trigger: ['blur']}]},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
label: '模型别名',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {rules: [{required: true, message: '请输入模型别名', trigger: ['blur']}]},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const keySchemas: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'apiKey',
|
||||||
|
label: 'Api Key',
|
||||||
|
labelMessage: '模型链接的秘钥,注意有些模型例如Gemini是本地认证方式,则不是通过这种方式',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {rules: [{required: true, message: '请输入API Key', trigger: ['blur']}]},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'baseUrl',
|
||||||
|
label: 'Base Url',
|
||||||
|
labelMessage: '注意对于大多数模型此参数仅代表中转地址,但是对于Ollama这类本地模型则是必填的',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: false,
|
||||||
|
trigger: ['blur'],
|
||||||
|
validator: (_, value: string) => {
|
||||||
|
const urlRegex = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/;
|
||||||
|
if (isNullOrWhitespace(value) || urlRegex.test(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return new Error('URL格式错误');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const openaiSchemas: FormSchema[] = [
|
||||||
|
...baseHeadSchemas,
|
||||||
|
{
|
||||||
|
field: 'model',
|
||||||
|
label: '模型',
|
||||||
|
labelMessage: '该LLM供应商对应的模型版本号',
|
||||||
|
component: 'Select',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{required: true, message: '请选择模型', trigger: ['blur']}]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
filterable: true,
|
||||||
|
options: getModels(ProviderEnum.OPENAI),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'imageSize',
|
||||||
|
label: '图片大小',
|
||||||
|
labelMessage: '生成图片的大小尺寸',
|
||||||
|
component: 'Select',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{required: true, message: '请选择图片大小', trigger: ['blur']}]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '1024x1024',
|
||||||
|
value: '1024x1024',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1024x1792',
|
||||||
|
value: '1024x1792',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1792x1024',
|
||||||
|
value: '1792x1024',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'imageQuality',
|
||||||
|
label: '图片质量',
|
||||||
|
labelMessage: '生成图片的质量',
|
||||||
|
component: 'Select',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{required: true, message: '请选择图片的质量', trigger: ['blur']}]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'standard',
|
||||||
|
value: 'standard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'hd',
|
||||||
|
value: 'hd',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'imageStyle',
|
||||||
|
label: '图片风格',
|
||||||
|
labelMessage: '生成图片的风格',
|
||||||
|
component: 'Select',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{required: true, message: '请选择图片的风格', trigger: ['blur']}]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'vivid',
|
||||||
|
value: 'vivid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'natural',
|
||||||
|
value: 'natural',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...keySchemas,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const azureOpenaiSchemas: FormSchema[] = [
|
||||||
|
...baseHeadSchemas,
|
||||||
|
{
|
||||||
|
field: 'model',
|
||||||
|
label: '模型',
|
||||||
|
labelMessage: '该LLM供应商对应的模型版本号',
|
||||||
|
component: 'Select',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{required: true, message: '请选择模型', trigger: ['blur']}]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
filterable: true,
|
||||||
|
options: getModels(ProviderEnum.AZURE_OPENAI),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'imageSize',
|
||||||
|
label: '图片大小',
|
||||||
|
labelMessage: '生成图片的大小尺寸',
|
||||||
|
component: 'Select',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{required: true, message: '请选择图片大小', trigger: ['blur']}]
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '1024x1024',
|
||||||
|
value: '1024x1024',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1024x1792',
|
||||||
|
value: '1024x1792',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1792x1024',
|
||||||
|
value: '1792x1024',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'imageQuality',
|
||||||
|
label: '图片质量',
|
||||||
|
labelMessage: '生成图片的质量',
|
||||||
|
component: 'Select',
|
||||||
|
formItemProps: {rules: [{required: true, message: '请选择图片的质量', trigger: ['blur']}]},
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'standard',
|
||||||
|
value: 'standard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'hd',
|
||||||
|
value: 'hd',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'imageStyle',
|
||||||
|
label: '图片风格',
|
||||||
|
labelMessage: '生成图片的风格',
|
||||||
|
component: 'Select',
|
||||||
|
formItemProps: {rules: [{required: true, message: '请选择图片的风格', trigger: ['blur']}]},
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'vivid',
|
||||||
|
value: 'vivid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'natural',
|
||||||
|
value: 'natural',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...keySchemas,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const zhipuSchemas: FormSchema[] = [
|
||||||
|
...baseHeadSchemas,
|
||||||
|
{
|
||||||
|
field: 'model',
|
||||||
|
label: '模型',
|
||||||
|
labelMessage: '该LLM供应商对应的模型版本号',
|
||||||
|
component: 'Select',
|
||||||
|
formItemProps: {rules: [{required: true, message: '请选择模型', trigger: ['blur']}]},
|
||||||
|
componentProps: {
|
||||||
|
filterable: true,
|
||||||
|
options: getModels(ProviderEnum.ZHIPU),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...keySchemas,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getSchemas(provider: string) {
|
||||||
|
switch (provider) {
|
||||||
|
case ProviderEnum.OPENAI: {
|
||||||
|
return openaiSchemas;
|
||||||
|
}
|
||||||
|
case ProviderEnum.AZURE_OPENAI: {
|
||||||
|
return azureOpenaiSchemas;
|
||||||
|
}
|
||||||
|
case ProviderEnum.ZHIPU: {
|
||||||
|
return zhipuSchemas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModels(provider: string) {
|
||||||
|
const arr = LLMProviders.filter((i) => i.model === provider);
|
||||||
|
if (arr.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return arr[0].models.map((i) => {
|
||||||
|
return {
|
||||||
|
label: i,
|
||||||
|
value: i,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {Form} from '@/components/Form'
|
||||||
|
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
|
||||||
|
schemas.value = []
|
||||||
|
nextTick(() => {
|
||||||
|
formRef.value?.clearForm()
|
||||||
|
formRef.value?.setSchema(schemas.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = async (data: any = {}) => {
|
||||||
|
visible.value = true
|
||||||
|
isEdit.value = !!data.id
|
||||||
|
await nextTick()
|
||||||
|
formRef.value.setValues(data)
|
||||||
|
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
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<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 :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>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {Delete, Plus, Edit } from "@element-plus/icons-vue";
|
||||||
|
import usePage from './composables/index'
|
||||||
|
import {LLMProviders} from "@/views/ai/model/image/composables/consts";
|
||||||
|
import EditCom from './edit.vue'
|
||||||
|
import {Table} from "@/components/Table";
|
||||||
|
|
||||||
|
const {columns, tableData, formData, editRef, open, loadData, handleEdit, handleDel, loading} = usePage()
|
||||||
|
|
||||||
|
const handleReload = () => {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<div class="children flex">
|
||||||
|
<el-scrollbar class="h-full w-300px pl-10px pr-20px">
|
||||||
|
<div
|
||||||
|
v-for="(item,index) in LLMProviders" :key="index"
|
||||||
|
:class="{active: formData.provider === item.model}" class="menu"
|
||||||
|
@click="formData.provider = item.model">
|
||||||
|
<span>{{ item.name }}</span>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
<div class="h-full p-20px">
|
||||||
|
<el-alert
|
||||||
|
class="w-full mb-10px min-alert"
|
||||||
|
show-icon
|
||||||
|
title="鉴于很多模型的文生图效果很差甚至没有,这里只建议使用OpenAI的DALL-E模型"
|
||||||
|
type="info"
|
||||||
|
/>
|
||||||
|
<el-button :icon="Plus" class="my-10px" type="primary" @click="open">新增图像模型
|
||||||
|
</el-button>
|
||||||
|
<Table
|
||||||
|
:columns="columns" :data="tableData" :loading="loading" 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>
|
||||||
|
<EditCom ref="editRef" @reload="handleReload"/>
|
||||||
|
</ContentWrap>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.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:nth-child(2) {
|
||||||
|
width: calc(100% - 300px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { Echart } from '@/components/Echart';
|
||||||
|
import { getReqChartBy30 } from '@/api/new-ai/statictic';
|
||||||
|
import {dayjs} from 'element-plus'
|
||||||
|
const options = ref({});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const data = await getReqChartBy30();
|
||||||
|
const xData: any = [];
|
||||||
|
const yData: any = [];
|
||||||
|
data.forEach((i: any) => {
|
||||||
|
xData.push(dayjs(i.date).format('YYYY-MM-DD'));
|
||||||
|
yData.push(i.tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
options.value = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
lineStyle: {
|
||||||
|
width: 1,
|
||||||
|
color: '#019680',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: xData,
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
width: 1,
|
||||||
|
type: 'solid',
|
||||||
|
color: 'rgba(226,226,226,0.5)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
splitNumber: 4,
|
||||||
|
axisTick: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
splitArea: {
|
||||||
|
show: true,
|
||||||
|
areaStyle: {
|
||||||
|
color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
grid: { left: '1%', right: '1%', top: '2%', bottom: 0, containLabel: true },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
smooth: true,
|
||||||
|
data: yData,
|
||||||
|
type: 'line',
|
||||||
|
areaStyle: {},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#5ab1ef',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3 class="my-2 mb-6 text-lg">近30天请求汇总</h3>
|
||||||
|
<Echart :options="options" height="240px" />
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!--
|
||||||
|
- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Delete } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
import { searchSchemas } from './columns'
|
||||||
|
import useOrder from '../composables'
|
||||||
|
import { columns } from './columns'
|
||||||
|
const { register, handleSearch, methods, tableObject, pagination } = useOrder()
|
||||||
|
|
||||||
|
const actionColumn = {
|
||||||
|
label: '操作',
|
||||||
|
field: 'action',
|
||||||
|
width: 70,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<el-card>
|
||||||
|
<Search :schema="searchSchemas" inline @search="handleSearch" />
|
||||||
|
|
||||||
|
<Table
|
||||||
|
:columns="[...columns, actionColumn]"
|
||||||
|
:data="tableObject.tableList"
|
||||||
|
:pagination="pagination"
|
||||||
|
@register="register"
|
||||||
|
>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<el-button :icon="Delete" text type="danger" @click="methods.delList(row.id, false)" />
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -0,0 +1,56 @@
|
||||||
|
import type { FormSchema } from '@/types/form';
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
{
|
||||||
|
label: '用户名',
|
||||||
|
field: 'username',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '模型名称',
|
||||||
|
field: 'model',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tokens',
|
||||||
|
field: 'tokens',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Prompt Tokens',
|
||||||
|
field: 'promptTokens',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Prompt Tokens',
|
||||||
|
field: 'promptTokens',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'IP地址',
|
||||||
|
field: 'ip',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '调用时间',
|
||||||
|
field: 'createTime',
|
||||||
|
align: 'center',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const searchSchemas: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
component: 'Input',
|
||||||
|
label: '用户名',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入用户名查询'
|
||||||
|
},
|
||||||
|
colProps: {
|
||||||
|
span: 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
]
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
import type { TableColumn } from '@/types/table'
|
||||||
|
import { useTable } from '@/hooks/web/useTable'
|
||||||
|
import { dayjs } from 'element-plus'
|
||||||
|
import {page, del} from '@/api/new-ai/message'
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const searchParams = ref({})
|
||||||
|
const { register, tableObject, methods } = useTable({
|
||||||
|
getListApi:page,
|
||||||
|
defaultParams: searchParams.value,
|
||||||
|
delListApi:del
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSearch = (values: any) => {
|
||||||
|
methods.setSearchParams(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagination = computed(() => {
|
||||||
|
return {
|
||||||
|
total: tableObject.total,
|
||||||
|
pageSize: tableObject.pageSize,
|
||||||
|
currentPage: tableObject.currentPage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
methods.getList()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
register,
|
||||||
|
handleSearch,
|
||||||
|
methods,
|
||||||
|
tableObject,
|
||||||
|
pagination
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Chart from './components/Chart.vue';
|
||||||
|
import List from './components/List.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="overflow-y-auto h-full">
|
||||||
|
<el-card>
|
||||||
|
<Chart />
|
||||||
|
<List />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,67 @@
|
||||||
|
<!--
|
||||||
|
- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref, toRaw } from 'vue'
|
||||||
|
import { AppApi } from '@/api/new-ai/app'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
id: any
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits(['update'])
|
||||||
|
const options = ref([])
|
||||||
|
const appId = ref('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
options.value = await AppApi.getAppList({})
|
||||||
|
appId.value = props.id
|
||||||
|
})
|
||||||
|
|
||||||
|
function onUpdate(val: any) {
|
||||||
|
const obj = toRaw(options.value.find(item => item.id === val))
|
||||||
|
if (obj == null) {
|
||||||
|
emit('update', {
|
||||||
|
id: '',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
emit('update', {
|
||||||
|
id: obj.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-select
|
||||||
|
v-model="appId"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择关联应用"
|
||||||
|
@change="onUpdate"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in options"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,96 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref, toRaw } from 'vue'
|
||||||
|
import { ModelApi } from '@/api/new-ai/model'
|
||||||
|
import { LLMProviders } from '@/views/ai/model/chatModel/composables/consts'
|
||||||
|
import { ModelTypeEnum } from '@/api/models'
|
||||||
|
import { useChatStore } from '@/views/ai/chat/new-chat/store/useChatStore'
|
||||||
|
|
||||||
|
const chatStore = useChatStore()
|
||||||
|
const props = defineProps<{
|
||||||
|
id: any
|
||||||
|
size?: 'large' | 'default' | 'small'
|
||||||
|
}>()
|
||||||
|
const size = props.size || 'default'
|
||||||
|
const emit = defineEmits(['update', 'load'])
|
||||||
|
const options = ref([])
|
||||||
|
const modelId = ref('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const providers = await ModelApi.getModelList({ type: ModelTypeEnum.CHAT })
|
||||||
|
if (chatStore.modelId === '' || chatStore.modelId === null) {
|
||||||
|
if (providers != null && providers.length != 0) {
|
||||||
|
const item = providers[0]
|
||||||
|
chatStore.modelId = item.id
|
||||||
|
chatStore.modelName = item.model
|
||||||
|
chatStore.modelProvider = item.provider
|
||||||
|
|
||||||
|
if (props.id == null) {
|
||||||
|
modelId.value = item.id
|
||||||
|
emit('load', item)
|
||||||
|
} else {
|
||||||
|
modelId.value = props.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const data: any = []
|
||||||
|
LLMProviders.forEach((i) => {
|
||||||
|
const children = providers.filter((m) => m.provider == i.model)
|
||||||
|
if (children.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.push({
|
||||||
|
label: i.name,
|
||||||
|
value: i.id,
|
||||||
|
children: children.map(child => ({
|
||||||
|
label: child.name,
|
||||||
|
value: child.id,
|
||||||
|
raw: child
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
options.value = data
|
||||||
|
modelId.value = chatStore.modelId
|
||||||
|
})
|
||||||
|
|
||||||
|
function onUpdate(val: any) {
|
||||||
|
console.log(val, 'val')
|
||||||
|
const group = options.value.find(g => g.children.some(c => c.value === val))
|
||||||
|
if (!group) return
|
||||||
|
console.log(group, 'group')
|
||||||
|
const option = group.children.find(c => c.value === val)
|
||||||
|
if (!option) return
|
||||||
|
const obj = option.raw
|
||||||
|
emit('update', {
|
||||||
|
id: obj.id,
|
||||||
|
modelName: obj.model,
|
||||||
|
modelProvider: obj.provider,
|
||||||
|
})
|
||||||
|
chatStore.modelId = obj.id
|
||||||
|
chatStore.modelName = obj.model
|
||||||
|
chatStore.modelProvider = obj.provider
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-select
|
||||||
|
v-model="modelId"
|
||||||
|
:size="size"
|
||||||
|
:props="{
|
||||||
|
expandTrigger: 'hover'
|
||||||
|
}"
|
||||||
|
placeholder="请选择关联模型"
|
||||||
|
@change="onUpdate"
|
||||||
|
>
|
||||||
|
<el-option-group v-for="group in options" :key="group.label" :label="group.label">
|
||||||
|
<el-option v-for="option in group.children" :key="option.value" :label="option.label" :value="option.value" />
|
||||||
|
</el-option-group>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-cascader) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,103 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="创建知识库" width="800px" :close-on-click-modal="!loading" :close-on-press-escape="!loading" @close="handleClose">
|
||||||
|
<Form :schema="formSchemas" v-if="visible" :model="formData" ref="formRef" @register="register"/>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { FormSchema } from '@/types/form';
|
||||||
|
import {Form} from '@/components/Form'
|
||||||
|
import { useForm } from '@/hooks/web/useForm';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { KnowledgeApi } from '@/api/new-ai/knowledge';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const { register, methods, elFormRef } = useForm();
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
const formData = ref({})
|
||||||
|
const emits = defineEmits(['reload'])
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await elFormRef.value?.validate()
|
||||||
|
const data = await methods.getFormData();
|
||||||
|
loading.value = true;
|
||||||
|
await KnowledgeApi.createKnowledge(data).finally(() => loading.value = false);
|
||||||
|
ElMessage.success('创建成功');
|
||||||
|
handleClose();
|
||||||
|
emits('reload');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建失败:', error);
|
||||||
|
ElMessage.error('创建失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
visible.value = false;
|
||||||
|
nextTick(() => {
|
||||||
|
formRef.value?.clearForm()
|
||||||
|
formRef.value?.setSchema([])
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchemas = computed(() => [
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
component: 'Input',
|
||||||
|
label: '知识库名称',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入知识库名称',
|
||||||
|
},
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请输入知识库名称', trigger: ['blur'] }],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'embedStoreId',
|
||||||
|
label: '向量数据库',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请选择关联向量数据库', trigger: ['blur'] }],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'embedModelId',
|
||||||
|
label: '向量模型',
|
||||||
|
component: 'Input',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请选择关联向量模型', trigger: ['blur'] }],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'des',
|
||||||
|
component: 'Input',
|
||||||
|
label: '知识库描述',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入知识库描述',
|
||||||
|
type: 'textarea',
|
||||||
|
autosize: {
|
||||||
|
minRows: 3,
|
||||||
|
maxRows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请输入知识库描述', trigger: ['blur'] }],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
])
|
||||||
|
const open = async () => {
|
||||||
|
visible.value = true;
|
||||||
|
}
|
||||||
|
// 对外暴露方法
|
||||||
|
defineExpose({
|
||||||
|
open
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script></script>
|
|
@ -0,0 +1,117 @@
|
||||||
|
<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, ElTag } from 'element-plus'
|
||||||
|
import { DocsApi } from '@/api/new-ai/docs'
|
||||||
|
import { Delete } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const tableRef = ref()
|
||||||
|
const docsList = ref([])
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
label: '文档名称',
|
||||||
|
field: 'name',
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '字符数',
|
||||||
|
field: 'wordNum',
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '切片内容',
|
||||||
|
field: 'content',
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '切片状态',
|
||||||
|
field: 'status',
|
||||||
|
width: 200,
|
||||||
|
formatter(row) {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{
|
||||||
|
size: 'small',
|
||||||
|
type: row.status == true ? 'success' : 'info'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => (row.status == true ? '已训练' : '未训练')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '切片时间',
|
||||||
|
field: 'createTime'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '操作',
|
||||||
|
field: 'action',
|
||||||
|
width: 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const { tableObject, register, tableMethods } = useTable({
|
||||||
|
getListApi: SliceApi.getSlicePage,
|
||||||
|
delListApi: SliceApi.deleteSlice,
|
||||||
|
defaultParams: {
|
||||||
|
knowledgeId: useRoute().params.id
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const schema = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '所属文档',
|
||||||
|
field: 'docId',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
style: {
|
||||||
|
width: '150px'
|
||||||
|
},
|
||||||
|
placeholder: '请选择所属文档',
|
||||||
|
options: docsList.value.map((item: any) => {
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
value: item.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const pagination = computed(() => {
|
||||||
|
return {
|
||||||
|
pageSize: tableObject.pageSize,
|
||||||
|
currentPage: tableObject.currentPage,
|
||||||
|
total: tableObject.total
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onMounted(async () => {
|
||||||
|
tableMethods.getList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Search :schema="schema" @search="tableMethods.getList()" />
|
||||||
|
<Table
|
||||||
|
class="mt-20px"
|
||||||
|
ref="tableRef"
|
||||||
|
border
|
||||||
|
:data="tableObject.tableList"
|
||||||
|
:columns="columns"
|
||||||
|
:schema="schema"
|
||||||
|
:label-width="100"
|
||||||
|
:pagination="pagination"
|
||||||
|
@register="register"
|
||||||
|
:search-table-height="true"
|
||||||
|
>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<el-button text :icon="Delete" type="danger" @click="tableMethods.delList(row.id, false)" />
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss"></style>
|
|
@ -0,0 +1,151 @@
|
||||||
|
<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, ElTag } from 'element-plus'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { Refresh, Delete, Edit } from '@element-plus/icons-vue'
|
||||||
|
import EditDoc from './edit-doc.vue'
|
||||||
|
const editDocRef = ref()
|
||||||
|
const route = useRoute()
|
||||||
|
const tableRef = ref()
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
label: '文档名称',
|
||||||
|
field: 'name',
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '文档链接',
|
||||||
|
field: 'url',
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '文档来源',
|
||||||
|
field: 'type',
|
||||||
|
width: 200,
|
||||||
|
formatter(row) {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{
|
||||||
|
size: 'small',
|
||||||
|
type: row.type == 'UPLOAD' ? 'success' : 'info',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => (row.type == 'UPLOAD' ? '上传' : '录入'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '切片数量',
|
||||||
|
field: 'sliceNum',
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '切片状态',
|
||||||
|
field: 'status',
|
||||||
|
width: 200,
|
||||||
|
formatter(row) {
|
||||||
|
return h(
|
||||||
|
ElTag,
|
||||||
|
{
|
||||||
|
size: 'small',
|
||||||
|
type: row.sliceStatus == true ? 'success' : 'info',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => (row.sliceStatus == true ? '已训练' : '未训练'),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '文件大小',
|
||||||
|
field: 'size',
|
||||||
|
formatter(rowData) {
|
||||||
|
return (Number(rowData.size) / 1000000).toFixed(2) + ' MB';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '操作',
|
||||||
|
field: 'action',
|
||||||
|
width: 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const { tableObject, register, tableMethods } = useTable({
|
||||||
|
getListApi: DocsApi.getDocsPage,
|
||||||
|
delListApi: DocsApi.deleteDocs,
|
||||||
|
defaultParams: {
|
||||||
|
knowledgeId: route.params.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const pagination = computed(() => {
|
||||||
|
return {
|
||||||
|
total: tableObject.total,
|
||||||
|
pageSize: tableObject.pageSize,
|
||||||
|
currentPage: tableObject.currentPage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const schema = ref<FormSchema[]>([
|
||||||
|
{
|
||||||
|
label: '文档名称',
|
||||||
|
field: 'name',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
style: {
|
||||||
|
width: '150px'
|
||||||
|
},
|
||||||
|
placeholder: '请输入文档名称'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 重新向量化
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const openEdit = (row) => {
|
||||||
|
editDocRef.value.show(route.params.id, row.id) // docId可选,用于编辑模式
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
tableMethods.getList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Search :schema="schema" @search="tableMethods.getList()" />
|
||||||
|
<Table
|
||||||
|
class="mt-20px"
|
||||||
|
ref="tableRef"
|
||||||
|
border
|
||||||
|
:data="tableObject.tableList"
|
||||||
|
:columns="columns"
|
||||||
|
:schema="schema"
|
||||||
|
:label-width="100"
|
||||||
|
:pagination="pagination"
|
||||||
|
:search-table-height="true"
|
||||||
|
@register="register"
|
||||||
|
>
|
||||||
|
<template #action="{ row }">
|
||||||
|
<el-button text type="primary" :icon="Refresh" @click="handleReEmbed(row)" />
|
||||||
|
<el-button text type="primary" :icon="Edit" @click="openEdit(row)" />
|
||||||
|
<el-button text :icon="Delete" type="danger" @click="tableMethods.delList(row.id, false)" />
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<edit-doc ref="editDocRef" @reload="tableMethods.getList()" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -0,0 +1,79 @@
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Document } from '@element-plus/icons-vue'
|
||||||
|
import { EmbeddingApi } from '@/api/new-ai/embedding'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
const { embeddingSearch, embeddingText } = EmbeddingApi
|
||||||
|
const route = useRoute()
|
||||||
|
const content = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref<any>([])
|
||||||
|
|
||||||
|
async function onSearch(flag = false) {
|
||||||
|
if (content.value === '' && flag) {
|
||||||
|
list.value = []
|
||||||
|
ElMessage.warning('请先输入搜索内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
list.value = await embeddingSearch({
|
||||||
|
content: content.value,
|
||||||
|
knowledgeId: route.params.id
|
||||||
|
})
|
||||||
|
console.log(list.value)
|
||||||
|
} catch (error) {
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full w-full">
|
||||||
|
<div class="flex gap-10px my-10px flex-wrap">
|
||||||
|
<el-card class="w-[calc(25%-10px)]">
|
||||||
|
<el-button class="w-full mb-10px" :loading="loading" type="primary" @click="onSearch(true)">向量搜索</el-button>
|
||||||
|
<el-input
|
||||||
|
v-model="content"
|
||||||
|
placeholder="请输入关键词查询向量文本"
|
||||||
|
:rows="10"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
<template v-if="list.length > 0">
|
||||||
|
<el-card
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item.index"
|
||||||
|
class="rounded-lg cursor-pointer w-[calc(25%-10px)] bg-[var(--el-bg-color-pages)]"
|
||||||
|
shadow="hover"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<el-icon>
|
||||||
|
<Document />
|
||||||
|
</el-icon>
|
||||||
|
<el-text class="truncate">{{ item.docsName }}</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-scrollbar height="200px" class="text-14px">
|
||||||
|
{{ item.text }}
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
<el-empty v-if="list.length === 0" class="my-4 flex-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
//a{
|
||||||
|
// color: var(--el-bg-color-page);
|
||||||
|
//}
|
||||||
|
</style>
|
|
@ -0,0 +1,97 @@
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Upload } from '@element-plus/icons-vue'
|
||||||
|
import { EmbeddingApi } from '@/api/new-ai/embedding'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const fileList = ref<any[]>([])
|
||||||
|
const uploadRef = ref()
|
||||||
|
|
||||||
|
const handleUpload = async (file: File) => {
|
||||||
|
const knowledgeId = route.params.id as string
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
await EmbeddingApi.embeddingDocs(knowledgeId, formData, (event: any) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
const progress = Math.round((event.loaded * 100) / event.total)
|
||||||
|
console.log('Upload progress:', progress)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
ElMessage.success('上传成功,文档解析中...')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload failed:', error)
|
||||||
|
ElMessage.error('上传失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExceed = () => {
|
||||||
|
ElMessage.warning('每次只能上传一个文件')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="upload-container">
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
class="upload-dragger"
|
||||||
|
drag
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
accept=".doc,.docx,.pdf,.txt,.md"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
:on-change="(file) => handleUpload(file.raw as File)"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload"><Upload /></el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
拖拽文件到此处或 <em>点击上传</em>
|
||||||
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
支持的文件格式:.txt、.md、.docx、.doc、.pdf
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.upload-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-dragger {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload-dragger) {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon--upload {
|
||||||
|
font-size: 48px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__text {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__text em {
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,117 @@
|
||||||
|
<!--
|
||||||
|
- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { nextTick, ref } from 'vue'
|
||||||
|
import { DocsApi } from '@/api/new-ai/docs'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { FormSchema } from '@/types/form'
|
||||||
|
import { isNullOrWhitespace } from '@/utils/is'
|
||||||
|
import { useForm } from '@/hooks/web/useForm'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const route = useRoute()
|
||||||
|
const emit = defineEmits(['reload'])
|
||||||
|
const showModal = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const { methods, elFormRef, register } = useForm()
|
||||||
|
const formRef = ref()
|
||||||
|
const editFlag = ref(false)
|
||||||
|
const formSchema = ref<FormSchema[]>([
|
||||||
|
{
|
||||||
|
label: '文档名称',
|
||||||
|
field: 'name',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入文档名称'
|
||||||
|
},
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请输入文档名称', trigger: ['blur'] }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '文档内容',
|
||||||
|
field: 'content',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入文档内容',
|
||||||
|
type: 'textarea',
|
||||||
|
autosize: {
|
||||||
|
minRows: 10,
|
||||||
|
maxRows: 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formItemProps: {
|
||||||
|
rules: [{ required: true, message: '请输入文档内容', trigger: ['blur'] }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
async function show(knowledgeId: string, id?: string) {
|
||||||
|
showModal.value = true
|
||||||
|
await nextTick()
|
||||||
|
if (id) {
|
||||||
|
editFlag.value = true
|
||||||
|
const res = await DocsApi.getDocsById(id)
|
||||||
|
methods.setValues(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
await elFormRef.value?.validate()
|
||||||
|
const formData =await methods.getFormData() as Record<string, any>
|
||||||
|
console.log(formData)
|
||||||
|
formData.knowledgeId = route.params.id as string
|
||||||
|
if (!formData.name) {
|
||||||
|
ElMessage.error('请完善表单')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isNullOrWhitespace(formData.id)) {
|
||||||
|
await DocsApi.addDocs(formData)
|
||||||
|
ElMessage.success('新增成功')
|
||||||
|
} else {
|
||||||
|
await DocsApi.updateDocs(formData)
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
}
|
||||||
|
showModal.value = false
|
||||||
|
emit('reload')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('操作失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="showModal"
|
||||||
|
:title="editFlag ? '编辑文档' : '新增文档'"
|
||||||
|
width="500px"
|
||||||
|
:close-on-click-modal="!loading"
|
||||||
|
:close-on-press-escape="!loading"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<Form :schema="formSchema" ref="formRef" @register="register" />
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showModal = false" :loading="loading">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
|
@ -1,151 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="upload-container">
|
|
||||||
<!-- 标题 -->
|
|
||||||
<div class="title">
|
|
||||||
<div>选择数据源</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 数据源选择 -->
|
|
||||||
<div class="resource-btn" >导入已有文本</div>
|
|
||||||
|
|
||||||
<!-- 上传文件区域 -->
|
|
||||||
<el-form>
|
|
||||||
<div class="upload-section">
|
|
||||||
<div class="upload-label">上传文本文件</div>
|
|
||||||
<el-upload
|
|
||||||
class="upload-area"
|
|
||||||
action="#"
|
|
||||||
:file-list="fileList"
|
|
||||||
:on-remove="handleRemove"
|
|
||||||
:before-upload="beforeUpload"
|
|
||||||
list-type="text"
|
|
||||||
drag
|
|
||||||
>
|
|
||||||
<i class="el-icon-upload"></i>
|
|
||||||
<div class="el-upload__text">拖拽文件至此,或者 <em>选择文件</em></div>
|
|
||||||
<div class="el-upload__tip">
|
|
||||||
已支持 TXT、MARKDOWN、PDF、HTML、XLSX、XLS、DOCX、CSV、EML、MSG、PPTX、PPT、XML、EPUB,每个文件不超过 15MB。
|
|
||||||
</div>
|
|
||||||
</el-upload>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 下一步按钮 -->
|
|
||||||
<div class="next-button">
|
|
||||||
<el-button type="primary" :disabled="!fileList.length">下一步</el-button>
|
|
||||||
</div>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<!-- 知识库创建 -->
|
|
||||||
<div class="create-knowledge">
|
|
||||||
<el-link type="primary" underline>创建一个空知识库</el-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const fileList = ref([])
|
|
||||||
|
|
||||||
const handleRemove = (file, fileList) => {
|
|
||||||
console.log(file, fileList)
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforeUpload = (file) => {
|
|
||||||
fileList.value.push(file)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.upload-container {
|
|
||||||
width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #ebebeb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-btn {
|
|
||||||
margin-top: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 150px;
|
|
||||||
border: 1.5px solid #528bff;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 30px;
|
|
||||||
color: #101828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-section {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-label {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-area {
|
|
||||||
margin-top: 10px;
|
|
||||||
border: 1px dashed #d9d9d9;
|
|
||||||
padding: 40px;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-upload__text em {
|
|
||||||
color: #409eff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-upload__tip {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-button {
|
|
||||||
text-align: left;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-knowledge {
|
|
||||||
text-align: left;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-form-item {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-radio-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-radio-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-radio-button .el-icon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue