parent
a49de52587
commit
46d23bd6b6
|
@ -29,7 +29,9 @@
|
|||
"@form-create/designer": "^3.2.6",
|
||||
"@form-create/element-ui": "^3.2.11",
|
||||
"@iconify/iconify": "^3.1.1",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||
"@videojs-player/vue": "^1.0.0",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
|
@ -53,6 +55,7 @@
|
|||
"jsencrypt": "^3.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"markmap-common": "^0.16.0",
|
||||
"markmap-lib": "^0.16.1",
|
||||
"markmap-toolbar": "^0.17.0",
|
||||
|
@ -67,6 +70,7 @@
|
|||
"sortablejs": "^1.15.3",
|
||||
"steady-xml": "^0.1.0",
|
||||
"url": "^0.11.3",
|
||||
"uuid": "^11.1.0",
|
||||
"video.js": "^7.21.5",
|
||||
"vue": "3.5.12",
|
||||
"vue-dompurify-html": "^4.1.4",
|
||||
|
@ -88,6 +92,7 @@
|
|||
"@types/nprogress": "^0.2.3",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/qs": "^6.9.12",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"@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: '/aigc/app/list', params })
|
||||
},
|
||||
|
||||
// 获取应用分页数据
|
||||
getAppPage: async (params: any) => {
|
||||
return await request.get({ url: '/aigc/app/page', params })
|
||||
},
|
||||
|
||||
// 获取应用详情
|
||||
getApp: async (id: string) => {
|
||||
return await request.get({ url: `/aigc/app/${id}` })
|
||||
},
|
||||
|
||||
// 根据模型ID获取应用
|
||||
getAppByModelId: async (id: string) => {
|
||||
return await request.get({ url: `/aigc/app/byModelId/${id}` })
|
||||
},
|
||||
|
||||
// 获取应用API通道
|
||||
getAppApiChannel: async (appId: string) => {
|
||||
return await request.get({ url: `/aigc/app/channel/api/${appId}` })
|
||||
},
|
||||
|
||||
// 新增应用
|
||||
createApp: async (data: any) => {
|
||||
return await request.post({ url: '/aigc/app', data })
|
||||
},
|
||||
|
||||
// 更新应用
|
||||
updateApp: async (data: any) => {
|
||||
return await request.put({ url: '/aigc/app', data })
|
||||
},
|
||||
|
||||
// 删除应用
|
||||
deleteApp: async (id: string) => {
|
||||
return await request.delete({ url: `/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: '/aigc/app/api/list', params })
|
||||
},
|
||||
|
||||
// 获取API分页数据
|
||||
getApiPage: async (params: any) => {
|
||||
return await request.get({ url: '/aigc/app/api/page', params })
|
||||
},
|
||||
|
||||
// 获取API详情
|
||||
getApi: async (id: string) => {
|
||||
return await request.get({ url: `/aigc/app/api/${id}` })
|
||||
},
|
||||
|
||||
// 新增API
|
||||
createApi: async (data: any) => {
|
||||
return await request.post({ url: '/aigc/app/api', data })
|
||||
},
|
||||
|
||||
// 更新API
|
||||
updateApi: async (data: any) => {
|
||||
return await request.put({ url: '/aigc/app/api', data })
|
||||
},
|
||||
|
||||
// 删除API
|
||||
deleteApi: async (id: string) => {
|
||||
return await request.delete({ url: `/aigc/app/api/${id}` })
|
||||
},
|
||||
|
||||
// 发布API
|
||||
publishApi: async (id: string) => {
|
||||
return await request.put({ url: `/aigc/app/api/publish/${id}` })
|
||||
},
|
||||
|
||||
// 下线API
|
||||
offlineApi: async (id: string) => {
|
||||
return await request.put({ url: `/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: '/aigc/chat/completions',
|
||||
data,
|
||||
signal: controller.signal,
|
||||
onDownloadProgress,
|
||||
isReturnNativeResponse: true
|
||||
})
|
||||
}
|
||||
|
||||
export function clean(conversationId: string | null) {
|
||||
return request.delete({
|
||||
url: `/aigc/chat/messages/clean/${conversationId}`
|
||||
})
|
||||
}
|
||||
|
||||
export function getMessages(conversationId?: string) {
|
||||
return request.get({
|
||||
url: `/aigc/chat/messages/${conversationId}`
|
||||
})
|
||||
}
|
||||
|
||||
export function getAppInfo(params: any) {
|
||||
return request.get({
|
||||
url: `/aigc/app/info`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getImageModels() {
|
||||
return request.get({
|
||||
url: '/aigc/chat/getImageModels'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生成图片
|
||||
*/
|
||||
export function genImage(data: any) {
|
||||
return request.post({
|
||||
url: '/aigc/chat/image',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 生成思维导图
|
||||
*/
|
||||
export function genMindMap(data: any) {
|
||||
return request.post({
|
||||
url: '/aigc/chat/mindmap',
|
||||
data
|
||||
})
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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: '/oss/policy'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
*/
|
||||
static list(params: any) {
|
||||
return request.get({
|
||||
url: '/oss/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*/
|
||||
static del(objectName: string) {
|
||||
return request.delete({
|
||||
url: `/oss/${objectName}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
static upload(data: FormData) {
|
||||
return request.post({
|
||||
url: '/aigc/oss/upload',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件访问URL
|
||||
*/
|
||||
static getUrl(objectName: string) {
|
||||
return request.get({
|
||||
url: `/oss/url/${objectName}`
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import pageError from '@/assets/svgs/404.svg'
|
|||
import networkError from '@/assets/svgs/500.svg'
|
||||
import noPermission from '@/assets/svgs/403.svg'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
defineOptions({ name: 'Error' })
|
||||
|
||||
interface ErrorMap {
|
||||
|
|
|
@ -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>
|
|
@ -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,96 @@
|
|||
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import projectSetting from '@/settings/projectSetting.ts';
|
||||
// import {ICrumbsSetting, IHeaderSetting, IMenuSetting, IMultiTabsSetting} from "../../../types/config";
|
||||
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,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);
|
||||
});
|
||||
}
|
|
@ -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,105 @@
|
|||
<!--
|
||||
- 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/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/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,
|
||||
conversationId: null
|
||||
})
|
||||
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,49 @@
|
|||
<!--
|
||||
- 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 { 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">
|
||||
<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,98 @@
|
|||
<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
|
||||
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,113 @@
|
|||
<!--
|
||||
- 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';
|
||||
|
||||
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);
|
||||
});
|
||||
`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 bg-white h-full overflow-auto rounded">
|
||||
<n-config-provider :hljs="hljs" class="flex flex-col gap-4">
|
||||
<div>
|
||||
<n-alert title="API URL(API接口格式遵循OpenAI格式)" type="info" />
|
||||
<div class="bg-[#18181c] mt-2 py-2 px-4 overflow-x-auto rounded">
|
||||
<n-code :code="url" class="text-white" language="JavaScript" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<n-alert title="Request" type="info" />
|
||||
<div class="bg-[#18181c] mt-2 py-2 px-4 overflow-x-auto rounded">
|
||||
<n-code :code="request" class="text-white" language="JavaScript" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<n-alert title="Response(Stream)" type="info" />
|
||||
<div class="bg-[#18181c] py-2 mt-2 px-4 overflow-x-auto rounded">
|
||||
<n-code :code="response" class="text-white" language="JavaScript" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<n-alert title="API请求示例" type="info" />
|
||||
<div class="bg-[#18181c] mt-2 py-2 px-4 overflow-x-auto rounded">
|
||||
<n-code :code="demo" class="text-white" language="javascript" />
|
||||
</div>
|
||||
</div>
|
||||
</n-config-provider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></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,151 @@
|
|||
<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'
|
||||
|
||||
const emit = defineEmits(['reload'])
|
||||
const visible = ref(false)
|
||||
const formData = ref({})
|
||||
const formRef = ref()
|
||||
const schemas = ref<FormSchema[]>([
|
||||
{
|
||||
field: 'name',
|
||||
label: '应用名称',
|
||||
component: 'Input',
|
||||
formItemProps: {
|
||||
required: true,
|
||||
rules: [
|
||||
{ required: true, message: '请输入应用名称' },
|
||||
{ min: 2, max: 20, message: '长度在2-20个字符' }
|
||||
]
|
||||
},
|
||||
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: '请输入应用描述'
|
||||
},
|
||||
|
||||
},
|
||||
{
|
||||
field: 'icon',
|
||||
label: '应用图标',
|
||||
component: 'Input',
|
||||
formItemProps: {
|
||||
required: false,
|
||||
},
|
||||
componentProps: {
|
||||
placeholder: '请输入应用图标'
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'modelId',
|
||||
label: '关联模型',
|
||||
component: 'Input',
|
||||
formItemProps: {
|
||||
required: false,
|
||||
},
|
||||
componentProps: {
|
||||
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 () => {
|
||||
try {
|
||||
const form = formRef.value.getElFormRef()
|
||||
await form.validate()
|
||||
const values = formRef.value.formModel
|
||||
loading.value = true
|
||||
|
||||
if (isEdit.value) {
|
||||
await AppApi.updateApp(values)
|
||||
ElMessage.success('更新应用成功')
|
||||
} else {
|
||||
await AppApi.createApp(values)
|
||||
ElMessage.success('创建应用成功')
|
||||
}
|
||||
close()
|
||||
emit('reload')
|
||||
} catch (error) {
|
||||
console.error('Failed to save app:', error)
|
||||
ElMessage.error(isEdit.value ? '更新应用失败' : '创建应用失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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 #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,189 @@
|
|||
<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[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '',
|
||||
description: '',
|
||||
apiKey: '',
|
||||
provider: '',
|
||||
channel: '',
|
||||
createTime: ''
|
||||
}
|
||||
])
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await AppApi.getAppList({})
|
||||
tableData.value = res.data || []
|
||||
} catch (error) {
|
||||
ElMessage.error('获取应用列表失败')
|
||||
console.error('Failed to fetch app list:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 新增应用
|
||||
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.concat(tableData).concat(tableData).concat(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.description || '暂无描述' }}</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-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,98 @@
|
|||
|
||||
|
||||
<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 id = route.params.id;
|
||||
const data = await getAppInfo({
|
||||
appId: id,
|
||||
conversationId: null,
|
||||
});
|
||||
form.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
</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 './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">
|
||||
<div 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>
|
||||
</div>
|
||||
</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,75 @@
|
|||
<!--
|
||||
- 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 { 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-3 flex flex-wrap justify-between items-center">
|
||||
<div class="font-bold flex justify-center items-center flex-wrap gap-2">
|
||||
<el-icon class="text-lg"><Star /></el-icon>
|
||||
<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" size="small" @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,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.
|
||||
-->
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import Chat from '@/views/ai/chat/Chat.vue';
|
||||
import { getMessages } from '@/api/new-ai/chat';
|
||||
import { useChatStore } from '@/views/ai/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.info.id;
|
||||
chatStore.messages = await getMessages(userStore.info.id);
|
||||
loading.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card class="p-4 pt-1 w-full h-full">
|
||||
<Header title="AI聊天助手" @reload="fetch" />
|
||||
<div class="flex flex-col w-full overflow-hidden" style="height: calc(100vh - 180px)">
|
||||
<main ref="contentRef" class="flex-1 overflow-hidden overflow-y-auto">
|
||||
<Chat />
|
||||
</main>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
::v-deep(.n-tabs.n-tabs--top .n-tab-pane) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,143 @@
|
|||
<!--
|
||||
- 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 { 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"
|
||||
>
|
||||
<el-icon v-if="inversion"><User /></el-icon>
|
||||
<el-icon v-else><Monitor /></el-icon>
|
||||
</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,139 @@
|
|||
<!--
|
||||
- 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 { 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">
|
||||
@import './styles.scss';
|
||||
</style>
|
|
@ -0,0 +1,122 @@
|
|||
@import 'highlight.js/styles/github.css';
|
||||
|
||||
.message-reply,
|
||||
.message-request {
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-reply {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.message-request {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.text-wrap {
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.code-block-wrapper {
|
||||
position: relative;
|
||||
padding-top: 24px;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
|
||||
&:hover {
|
||||
.code-block-header__copy {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 4px 0;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 8px 8px 0 0;
|
||||
|
||||
&__lang {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&__copy {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
|
||||
&:hover {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code-block-body {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
background-color: transparent;
|
||||
margin-bottom: 0;
|
||||
|
||||
pre {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
border-collapse: collapse;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0% {
|
||||
background: #fff;
|
||||
}
|
||||
50% {
|
||||
background: #000;
|
||||
}
|
||||
100% {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-blink {
|
||||
animation: blink 1s infinite;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
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,19 @@
|
|||
/*
|
||||
* 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 "github-markdown.less";
|
||||
@import "highlight.less";
|
||||
@import "style.less";
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.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/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,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,107 @@
|
|||
<!--
|
||||
- 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 { 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/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) {
|
||||
const group = options.value.find(g => g.children.some(c => c.value === val))
|
||||
if (!group) return
|
||||
|
||||
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-cascader
|
||||
v-model="modelId"
|
||||
:options="options"
|
||||
:size="size"
|
||||
:props="{
|
||||
expandTrigger: 'hover'
|
||||
}"
|
||||
placeholder="请选择关联模型"
|
||||
@change="onUpdate"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-cascader) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 ProjectSettingState {
|
||||
//导航模式
|
||||
navMode: string;
|
||||
//导航风格
|
||||
navTheme: string;
|
||||
//顶部设置
|
||||
headerSetting: object;
|
||||
//页脚
|
||||
showFooter: boolean;
|
||||
//菜单设置
|
||||
menuSetting: object;
|
||||
//多标签
|
||||
multiTabsSetting: object;
|
||||
//面包屑
|
||||
crumbsSetting: object;
|
||||
//权限模式
|
||||
permissionMode: string;
|
||||
}
|
||||
|
||||
export interface IBodySetting {
|
||||
fixed: boolean;
|
||||
}
|
||||
|
||||
export interface IHeaderSetting {
|
||||
bgColor: string;
|
||||
fixed: boolean;
|
||||
isReload: boolean;
|
||||
}
|
||||
|
||||
export interface IMenuSetting {
|
||||
minMenuWidth: number;
|
||||
menuWidth: number;
|
||||
fixed: boolean;
|
||||
mixMenu: boolean;
|
||||
collapsed: boolean;
|
||||
mobileWidth: number;
|
||||
}
|
||||
|
||||
export interface ICrumbsSetting {
|
||||
show: boolean;
|
||||
showIcon: boolean;
|
||||
}
|
||||
|
||||
export interface IMultiTabsSetting {
|
||||
bgColor: string;
|
||||
fixed: boolean;
|
||||
show: boolean;
|
||||
}
|
||||
export interface GlobConfig {
|
||||
title: string;
|
||||
apiUrl: string;
|
||||
shortName: string;
|
||||
urlPrefix?: string;
|
||||
uploadUrl?: string;
|
||||
imgUrl?: string;
|
||||
}
|
||||
|
||||
export interface GlobEnvConfig {
|
||||
// 标题
|
||||
VITE_GLOB_APP_TITLE: string;
|
||||
// 接口地址
|
||||
VITE_GLOB_API_URL: string;
|
||||
// 接口前缀
|
||||
VITE_GLOB_API_URL_PREFIX?: string;
|
||||
// Project abbreviation
|
||||
VITE_GLOB_APP_SHORT_NAME: string;
|
||||
// 图片上传地址
|
||||
VITE_GLOB_UPLOAD_URL?: string;
|
||||
//图片前缀地址
|
||||
VITE_GLOB_IMG_URL?: string;
|
||||
}
|
|
@ -59,7 +59,11 @@ export default ({command, mode}: ConfigEnv): UserConfig => {
|
|||
{
|
||||
find: /\@\//,
|
||||
replacement: `${pathResolve('src')}/`
|
||||
}
|
||||
},
|
||||
{
|
||||
find: /\/#\//,
|
||||
replacement: pathResolve('types') + '/',
|
||||
},
|
||||
]
|
||||
},
|
||||
build: {
|
||||
|
|
Loading…
Reference in New Issue