AIGC应用平台+知识库模块
BIN
jeecgboot-vue3/src/assets/images/ai/aiflow.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
81
jeecgboot-vue3/src/views/super/airag/aiapp/AiApp.api.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
export enum Api {
|
||||
//知识库管理
|
||||
list = '/airag/app/list',
|
||||
save = '/airag/app/edit',
|
||||
delete = '/airag/app/delete',
|
||||
queryById = '/airag/app/queryById',
|
||||
queryBathById = '/airag/knowledge/query/batch/byId',
|
||||
queryFlowById = '/airag/flow/queryById',
|
||||
promptGenerate = '/airag/app/prompt/generate',
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询应用
|
||||
* @param params
|
||||
*/
|
||||
export const appList = (params) => {
|
||||
return defHttp.get({ url: Api.list, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询知识库
|
||||
* @param params
|
||||
*/
|
||||
export const queryKnowledgeBathById = (params) => {
|
||||
return defHttp.get({ url: Api.queryBathById, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据应用id查询应用
|
||||
* @param params
|
||||
*/
|
||||
export const queryById = (params) => {
|
||||
return defHttp.get({ url: Api.queryById, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增应用
|
||||
* @param params
|
||||
*/
|
||||
export const saveApp = (params) => {
|
||||
return defHttp.put({ url: Api.save, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除应用
|
||||
* @param params
|
||||
* @param handleSuccess
|
||||
*/
|
||||
export const deleteApp = (params, handleSuccess) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '是否删除名称为'+params.name+'的应用吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
return defHttp.delete({ url: Api.delete, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 根据应用id查询流程
|
||||
* @param params
|
||||
*/
|
||||
export const queryFlowById = (params) => {
|
||||
return defHttp.get({ url: Api.queryFlowById, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用编排
|
||||
* @param params
|
||||
*/
|
||||
export const promptGenerate = (params) => {
|
||||
return defHttp.get({ url: Api.promptGenerate, params,timeout: 5 * 60 * 1000 }, { isTransformResponse: false });
|
||||
};
|
88
jeecgboot-vue3/src/views/super/airag/aiapp/AiApp.data.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { FormSchema } from '@/components/Form';
|
||||
|
||||
/**
|
||||
* 表单
|
||||
*/
|
||||
export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: 'id',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '应用名称',
|
||||
field: 'name',
|
||||
required: true,
|
||||
componentProps: {
|
||||
//是否展示字数
|
||||
showCount: true,
|
||||
maxlength: 64,
|
||||
},
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '应用描述',
|
||||
field: 'descr',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '描述该应用的应用场景及用途',
|
||||
rows: 4,
|
||||
//是否展示字数
|
||||
showCount: true,
|
||||
maxlength: 256,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '应用图标',
|
||||
field: 'icon',
|
||||
component: 'JImageUpload',
|
||||
},
|
||||
{
|
||||
label: '选择应用类型',
|
||||
field: 'type',
|
||||
component: 'Input',
|
||||
ifShow:({ values })=>{
|
||||
return !values.id;
|
||||
},
|
||||
slot: 'typeSlot',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 快捷指令表单
|
||||
*/
|
||||
export const quickCommandFormSchema: FormSchema[] = [
|
||||
{
|
||||
label: 'key',
|
||||
field: 'key',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '按钮名称',
|
||||
field: 'name',
|
||||
required: true,
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
showCount: true,
|
||||
maxLength: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '按钮图标',
|
||||
field: 'icon',
|
||||
component: 'IconPicker',
|
||||
},
|
||||
{
|
||||
label: '指令内容',
|
||||
field: 'descr',
|
||||
required: true,
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
autosize: { minRows: 4, maxRows: 4 },
|
||||
showCount: true,
|
||||
maxLength: 100,
|
||||
}
|
||||
},
|
||||
];
|
494
jeecgboot-vue3/src/views/super/airag/aiapp/AiAppList.vue
Normal file
@ -0,0 +1,494 @@
|
||||
<!--知识库文档列表-->
|
||||
<template>
|
||||
<div class="p-2 knowledge">
|
||||
<!--查询区域-->
|
||||
<div class="jeecg-basic-table-form-container">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
@keyup.enter.native="reload"
|
||||
:model="queryParam"
|
||||
:label-col="labelCol"
|
||||
:wrapper-col="wrapperCol"
|
||||
style="background-color: #f7f8fc"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :lg="6">
|
||||
<a-form-item name="name" label="应用名称">
|
||||
<JInput v-model:value="queryParam.name" placeholder="请输入应用名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6">
|
||||
<a-form-item name="type" label="应用类型">
|
||||
<j-dict-select-tag v-model:value="queryParam.type" dict-code="ai_app_type" placeholder="请选择应用类型" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xl="6" :lg="7" :md="8" :sm="24">
|
||||
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
|
||||
<a-col :lg="6">
|
||||
<a-button type="primary" preIcon="ant-design:search-outlined" @click="reload">查询</a-button>
|
||||
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
|
||||
</a-col>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
<a-row :span="24" class="knowledge-row">
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
|
||||
<a-card class="add-knowledge-card" :bodyStyle="cardBodyStyle">
|
||||
<span style="line-height: 18px; font-weight: 500; color: #676f83; font-size: 12px">创建应用</span>
|
||||
<div class="add-knowledge-doc" @click="handleCreateApp">
|
||||
<Icon icon="ant-design:form-outlined" size="13"></Icon>
|
||||
<span>创建空白应用</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" v-for="item in knowledgeAppDataList">
|
||||
<a-card class="knowledge-card pointer" @click="handleEditClick(item)">
|
||||
<div class="flex">
|
||||
<img class="header-img" :src="getImage(item.icon)" />
|
||||
<div class="header-text">
|
||||
<span class="header-text-top header-name ellipsis"> {{ item.name }} </span>
|
||||
<span class="header-text-top header-create ellipsis"> 创建者:{{ item.createBy }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-tag">
|
||||
<a-tag color="#EBF1FF" style="margin-right: 0" v-if="item.type === 'chatSimple'">
|
||||
<span style="color: #3370ff">简单配置</span>
|
||||
</a-tag>
|
||||
<a-tag color="#FDF6EC" style="margin-right: 0" v-if="item.type === 'chatFLow'">
|
||||
<span style="color: #e6a343">高级编排</span>
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="card-description">
|
||||
<span>{{ item.descr || '暂无描述' }}</span>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a-tooltip title="演示">
|
||||
<div class="card-footer-icon" @click.prevent.stop="handleViewClick(item.id)">
|
||||
<Icon class="operation" icon="ant-design:youtube-outlined" size="20" color="#1F2329"></Icon>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" style="float: left" />
|
||||
<a-tooltip title="删除">
|
||||
<div class="card-footer-icon" @click.prevent.stop="handleDeleteClick(item)">
|
||||
<Icon icon="ant-design:delete-outlined" class="operation" size="20" color="#1F2329"></Icon>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" style="float: left" />
|
||||
<a-tooltip title="发布">
|
||||
<a-dropdown class="card-footer-icon" placement="bottomRight" :trigger="['click']">
|
||||
<div @click.prevent.stop>
|
||||
<Icon icon="ant-design:send-outlined"></Icon>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="web" @click.prevent.stop="handleSendClick(item,'web')">
|
||||
<Icon icon="ant-design:dribbble-outlined" size="16"></Icon>
|
||||
嵌入网站
|
||||
</a-menu-item>
|
||||
<a-menu-item key="menu" @click.prevent.stop="handleSendClick(item,'menu')">
|
||||
<Icon icon="ant-design:menu-outlined" size="16"></Icon> 配置菜单
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<Pagination
|
||||
v-if="knowledgeAppDataList.length > 0"
|
||||
:current="pageNo"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:total="total"
|
||||
:showQuickJumper="true"
|
||||
:showSizeChanger="true"
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
/>
|
||||
<!-- Ai新增弹窗 -->
|
||||
<AiAppModal @register="registerModal" @success="handleSuccess"></AiAppModal>
|
||||
<!-- Ai设置弹窗 -->
|
||||
<AiAppSettingModal @register="registerSettingModal" @success="reload"></AiAppSettingModal>
|
||||
<!-- 发布弹窗 -->
|
||||
<AiAppSendModal @register="registerAiAppSendModal"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import { Avatar, Modal, Pagination } from 'ant-design-vue';
|
||||
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
|
||||
import defaultImg from './img/ailogo.png';
|
||||
import AiAppModal from './components/AiAppModal.vue';
|
||||
import AiAppSettingModal from './components/AiAppSettingModal.vue';
|
||||
import AiAppSendModal from './components/AiAppSendModal.vue';
|
||||
import Icon from '@/components/Icon';
|
||||
import { appList, deleteApp } from './AiApp.api';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
|
||||
import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
|
||||
|
||||
export default {
|
||||
name: 'AiAppList',
|
||||
components: {
|
||||
JDictSelectTag,
|
||||
JInput,
|
||||
AiAppSendModal,
|
||||
Icon,
|
||||
Pagination,
|
||||
Avatar,
|
||||
LoadingOutlined,
|
||||
BasicModal,
|
||||
AiAppModal,
|
||||
AiAppSettingModal,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
/**
|
||||
* 创建应用的集合
|
||||
*/
|
||||
const knowledgeAppDataList = ref<any>([]);
|
||||
//当前页数
|
||||
const pageNo = ref<number>(1);
|
||||
//每页条数
|
||||
const pageSize = ref<number>(10);
|
||||
//总条数
|
||||
const total = ref<number>(0);
|
||||
//可选择的页数
|
||||
const pageSizeOptions = ref<any>(['10', '20', '30']);
|
||||
//注册modal
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [registerSettingModal, { openModal: openAppModal }] = useModal();
|
||||
const [registerAiAppSendModal, { openModal: openAiAppSendModal }] = useModal();
|
||||
const { createMessage } = useMessage();
|
||||
//查询参数
|
||||
const queryParam = reactive<any>({});
|
||||
//查询区域label宽度
|
||||
const labelCol = reactive({
|
||||
xs: 24,
|
||||
sm: 4,
|
||||
xl: 6,
|
||||
xxl: 6,
|
||||
});
|
||||
//查询区域组件宽度
|
||||
const wrapperCol = reactive({
|
||||
xs: 24,
|
||||
sm: 20,
|
||||
});
|
||||
//表单的ref
|
||||
const formRef = ref();
|
||||
|
||||
reload();
|
||||
|
||||
/**
|
||||
* 加载数据
|
||||
*/
|
||||
function reload() {
|
||||
let params = {
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
column: 'createTime',
|
||||
order: 'desc',
|
||||
};
|
||||
Object.assign(params, queryParam);
|
||||
appList(params).then((res) => {
|
||||
if (res.success) {
|
||||
knowledgeAppDataList.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
} else {
|
||||
knowledgeAppDataList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建应用
|
||||
*/
|
||||
function handleCreateApp() {
|
||||
openModal(true, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页改变事件
|
||||
* @param page
|
||||
* @param current
|
||||
*/
|
||||
function handlePageChange(page, current) {
|
||||
pageNo.value = page;
|
||||
pageSize.value = current;
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功
|
||||
*/
|
||||
function handleSuccess(id) {
|
||||
reload();
|
||||
//打开编辑弹窗
|
||||
openAppModal(true, {
|
||||
isUpdate: false,
|
||||
id: id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片
|
||||
* @param url
|
||||
*/
|
||||
function getImage(url) {
|
||||
return url ? getFileAccessHttpUrl(url) : defaultImg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
* @param item
|
||||
*/
|
||||
function handleEditClick(item) {
|
||||
console.log('item:::', item);
|
||||
openAppModal(true, {
|
||||
isUpdate: true,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 演示
|
||||
*/
|
||||
function handleViewClick(id) {
|
||||
window.open('/ai/app/chat/' + id , '_blank');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
function handleDeleteClick(item) {
|
||||
deleteApp({ id: item.id, name: item.name }, reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布点击事件
|
||||
* @param item 数据
|
||||
* @param type 类别
|
||||
*/
|
||||
function handleSendClick(item,type) {
|
||||
openAiAppSendModal(true,{
|
||||
type: type,
|
||||
data: item
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function searchReset() {
|
||||
formRef.value.resetFields();
|
||||
queryParam.name = '';
|
||||
//刷新数据
|
||||
reload();
|
||||
}
|
||||
|
||||
return {
|
||||
handleCreateApp,
|
||||
knowledgeAppDataList,
|
||||
pageNo,
|
||||
pageSize,
|
||||
total,
|
||||
pageSizeOptions,
|
||||
handlePageChange,
|
||||
cardBodyStyle: { textAlign: 'left', width: '100%' },
|
||||
registerModal,
|
||||
handleSuccess,
|
||||
getImage,
|
||||
handleEditClick,
|
||||
handleViewClick,
|
||||
handleDeleteClick,
|
||||
registerSettingModal,
|
||||
reload,
|
||||
queryParam,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
handleSendClick,
|
||||
registerAiAppSendModal,
|
||||
searchReset,
|
||||
formRef,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.knowledge {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
background: #f7f8fc;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.add-knowledge-card {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: inline-flex;
|
||||
font-size: 16px;
|
||||
height: 152px;
|
||||
width: calc(100% - 20px);
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
.add-knowledge-card-icon {
|
||||
padding: 8px;
|
||||
color: #1f2329;
|
||||
background-color: #f5f6f7;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-card {
|
||||
border-radius: 10px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
height: 152px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
.header-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.header-text {
|
||||
margin-left: 5px;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
display: grid;
|
||||
width: calc(100% - 100px);
|
||||
.header-name {
|
||||
font-weight: bold;
|
||||
color: #354052;
|
||||
}
|
||||
.header-create {
|
||||
font-size: 12px;
|
||||
color: #646a73;
|
||||
}
|
||||
}
|
||||
.header-tag {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-knowledge-card,
|
||||
.knowledge-card {
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.add-knowledge-card:hover,
|
||||
.knowledge-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.knowledge-row {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.add-knowledge-doc {
|
||||
margin-top: 6px;
|
||||
color: #6f6f83;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
span {
|
||||
margin-left: 4px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
.add-knowledge-doc:hover {
|
||||
background: #c8ceda33;
|
||||
}
|
||||
.card-description {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
height: 4.5em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: #676f83;
|
||||
}
|
||||
.card-footer {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 0;
|
||||
min-height: 30px;
|
||||
padding: 0 16px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.card-footer-icon {
|
||||
font-size: 14px;
|
||||
height: 24px;
|
||||
padding: 0 7px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
float: left;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.card-footer-icon:hover {
|
||||
color: #000000;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.operation {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
.list-footer {
|
||||
text-align: right;
|
||||
margin-top: 5px;
|
||||
}
|
||||
:deep(.ant-card .ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
.ellipsis{
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.jeecg-basic-table-form-container {
|
||||
padding: 0;
|
||||
:deep(.ant-form) {
|
||||
background-color: transparent;
|
||||
}
|
||||
.table-page-search-submitButtons {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.airag-knowledge-doc .scroll-container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
372
jeecgboot-vue3/src/views/super/airag/aiapp/chat/AiChat.vue
Normal file
@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<div ref="chatContainerRef" class="chat-container" :style="chatContainerStyle">
|
||||
<template v-if="dataSource">
|
||||
<div class="leftArea" :class="[expand ? 'expand' : 'shrink']">
|
||||
<div class="content">
|
||||
<slide v-if="uuid" :dataSource="dataSource" @save="handleSave" :prologue="prologue" :appData="appData" @click="handleChatClick"></slide>
|
||||
</div>
|
||||
<div class="toggle-btn" @click="handleToggle">
|
||||
<span class="icon">
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rightArea" :class="[expand ? 'expand' : 'shrink']">
|
||||
<chat
|
||||
url="/airag/chat/send"
|
||||
v-if="uuid && chatVisible"
|
||||
:uuid="uuid"
|
||||
:historyData="chatData"
|
||||
type="view"
|
||||
@save="handleSave"
|
||||
:formState="appData"
|
||||
:prologue="prologue"
|
||||
:presetQuestion="presetQuestion"
|
||||
@reload-message-title="reloadMessageTitle"
|
||||
:chatTitle="chatTitle"
|
||||
:quickCommandData="quickCommandData"
|
||||
></chat>
|
||||
</div>
|
||||
</template>
|
||||
<Spin v-else :spinning="true"></Spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import slide from './slide.vue';
|
||||
import chat from './chat.vue';
|
||||
import { Spin } from 'ant-design-vue';
|
||||
import { ref, watch, nextTick, onUnmounted, onMounted } from 'vue';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { JEECG_CHAT_KEY } from '/@/enums/cacheEnum';
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const userId = useUserStore().getUserInfo?.id;
|
||||
const localKey = JEECG_CHAT_KEY + userId;
|
||||
let timer: any = null;
|
||||
let unwatch01: any = null;
|
||||
const dataSource = ref<any>({});
|
||||
const uuid = ref<string>('');
|
||||
const chatData = ref<any>([]);
|
||||
const expand = ref<any>(true);
|
||||
const chatVisible = ref(true);
|
||||
const chatContainerRef = ref<any>(null);
|
||||
const chatContainerStyle = ref({});
|
||||
//左侧聊天信息
|
||||
const chatTitle = ref<string>('');
|
||||
//左侧聊天点击的坐标
|
||||
const chatActiveKey = ref<number>(0);
|
||||
//预置开场白
|
||||
const presetQuestion = ref<string>('');
|
||||
|
||||
const handleToggle = () => {
|
||||
expand.value = !expand.value;
|
||||
};
|
||||
//应用id
|
||||
const appId = ref<string>('');
|
||||
//应用数据
|
||||
const appData = ref<any>({});
|
||||
//开场白
|
||||
const prologue = ref<string>('');
|
||||
//快捷指令
|
||||
const quickCommandData = ref<any>([]);
|
||||
|
||||
const priming = () => {
|
||||
dataSource.value = {
|
||||
active: '1002',
|
||||
usingContext: true,
|
||||
history: [{ id: '1002', title: '新建聊天', isEdit: false, disabled: true }],
|
||||
};
|
||||
chatTitle.value = '新建聊天';
|
||||
chatActiveKey.value = 0;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
// 删除标签或清空内容之后的保存
|
||||
//save(dataSource.value);
|
||||
setTimeout(() => {
|
||||
// 删除标签或清空内容也会触发watch保存,此时不需watch保存需清除
|
||||
//clearTimeout(timer);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
// 监听dataSource变化执行操作
|
||||
const execute = () => {
|
||||
unwatch01 = watch(
|
||||
() => dataSource.value.active,
|
||||
(value) => {
|
||||
if (value) {
|
||||
if (value == '1002') {
|
||||
uuid.value = '1002';
|
||||
chatData.value = [];
|
||||
chatTitle.value = "新建聊天";
|
||||
chatVisible.value = false;
|
||||
nextTick(() => {
|
||||
chatVisible.value = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-03-14---for:【QQYUN-11421】聊天,删除会话后,聊天切换到新的会话,但是聊天标题没有变---
|
||||
let values = dataSource.value.history.filter((item) => item.id === value);
|
||||
if(values && values.length>0){
|
||||
chatTitle.value = values[0]?.title
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-14---for:【QQYUN-11421】聊天,删除会话后,聊天切换到新的会话,但是聊天标题没有变---
|
||||
//根据选中的id查询聊天内容
|
||||
let params = { conversationId: value };
|
||||
uuid.value = value;
|
||||
defHttp.get({ url: '/airag/chat/messages', params }, { isTransformResponse: false }).then((res) => {
|
||||
if (res.success) {
|
||||
chatData.value = res.result;
|
||||
} else {
|
||||
chatData.value = [];
|
||||
}
|
||||
chatVisible.value = false;
|
||||
nextTick(() => {
|
||||
chatVisible.value = true;
|
||||
});
|
||||
});
|
||||
}else{
|
||||
chatData.value = [];
|
||||
chatTitle.value = "";
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化聊天信息
|
||||
* @param appId
|
||||
*/
|
||||
function initChartData(appId = '') {
|
||||
defHttp
|
||||
.get(
|
||||
{
|
||||
url: '/airag/chat/conversations',
|
||||
params: { appId: appId },
|
||||
},
|
||||
{ isTransformResponse: false }
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.success && res.result && res.result.length > 0) {
|
||||
dataSource.value.history = res.result;
|
||||
dataSource.value.active = res.result[0].id;
|
||||
chatTitle.value = res.result[0].title;
|
||||
chatActiveKey.value = 0;
|
||||
} else {
|
||||
priming();
|
||||
}
|
||||
!unwatch01 && execute();
|
||||
})
|
||||
.catch(() => {
|
||||
priming();
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
let params: any = router.currentRoute.value.params;
|
||||
if (params.appId) {
|
||||
appId.value = params.appId;
|
||||
getApplicationData(params.appId);
|
||||
initChartData(params.appId);
|
||||
} else {
|
||||
initChartData();
|
||||
quickCommandData.value = [
|
||||
{ name: '请介绍一下JeecgBoot', descr: "请介绍一下JeecgBoot" },
|
||||
{ name: 'JEECG有哪些优势?', descr: "JEECG有哪些优势?" },
|
||||
{ name: 'JEECG可以做哪些事情?', descr: "JEECG可以做哪些事情?" },];
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
chatData.value = [];
|
||||
chatTitle.value = "";
|
||||
prologue.value = ""
|
||||
presetQuestion.value = "";
|
||||
quickCommandData.value = [];
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取应用id
|
||||
*
|
||||
* @param appId
|
||||
*/
|
||||
async function getApplicationData(appId) {
|
||||
await defHttp
|
||||
.get(
|
||||
{
|
||||
url: '/airag/app/queryById',
|
||||
params: { id: appId },
|
||||
},
|
||||
{ isTransformResponse: false }
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
appData.value = res.result;
|
||||
if (res.result && res.result.prologue) {
|
||||
prologue.value = res.result.prologue;
|
||||
}
|
||||
if (res.result && res.result.quickCommand) {
|
||||
quickCommandData.value = JSON.parse(res.result.quickCommand);
|
||||
}
|
||||
if (res.result && res.result.presetQuestion) {
|
||||
presetQuestion.value = res.result.presetQuestion;
|
||||
}
|
||||
} else {
|
||||
appData.value = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 左侧消息列表点击事件
|
||||
* @param title
|
||||
* @param index
|
||||
*/
|
||||
function handleChatClick(title, index) {
|
||||
chatTitle.value = title;
|
||||
chatActiveKey.value = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载标题消息
|
||||
* @param text
|
||||
*/
|
||||
function reloadMessageTitle(text) {
|
||||
let title = dataSource.value.history[chatActiveKey.value].title;
|
||||
if(title === '新建聊天'){
|
||||
dataSource.value.history[chatActiveKey.value].title = text;
|
||||
dataSource.value.history[chatActiveKey.value]['disabled'] = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化聊天:用于icon点击
|
||||
*/
|
||||
function initChat(value) {
|
||||
appId.value = value;
|
||||
getApplicationData(value);
|
||||
initChartData(value);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
initChat
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unwatch01 && unwatch01();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => chatContainerRef.value,
|
||||
() => {
|
||||
if(chatContainerRef.value.offsetHeight){
|
||||
chatContainerStyle.value = { height: `${chatContainerRef.value.offsetHeight} px` };
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@width: 260px;
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: white;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
z-index: 999;
|
||||
border: 1px solid #eeeeee;
|
||||
:deep(.ant-spin) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.leftArea {
|
||||
width: @width;
|
||||
transition: 0.3s left;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.shrink {
|
||||
left: -@width;
|
||||
|
||||
.toggle-btn {
|
||||
.icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
transition:
|
||||
color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
right 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
color: rgb(51, 54, 57);
|
||||
border: 1px solid rgb(239, 239, 245);
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.06);
|
||||
transform: translateX(50%) translateY(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform: rotate(180deg);
|
||||
font-size: 18px;
|
||||
height: 18px;
|
||||
|
||||
svg {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rightArea {
|
||||
margin-left: @width;
|
||||
transition: 0.3s margin-left;
|
||||
|
||||
&.shrink {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="footer">
|
||||
<div v-if="!showChat" class="footer-icon" @click="chatClick">
|
||||
<Icon icon="ant-design:comment-outlined" size="22"></Icon>
|
||||
</div>
|
||||
<div v-if="showChat" class="footer-close-icon" @click="chatClick">
|
||||
<Icon icon="ant-design:close-outlined" size="20"></Icon>
|
||||
</div>
|
||||
<div v-if="showChat" class="ai-chat">
|
||||
<AiChat ref="aiChatRef"></AiChat>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import AiChat from './AiChat.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
//aiChat的ref
|
||||
const aiChatRef = ref();
|
||||
//应用id
|
||||
const appId = ref<string>('');
|
||||
|
||||
//是否显示聊天
|
||||
const showChat = ref<any>(false);
|
||||
const router = useRouter();
|
||||
//判断是否为初始化
|
||||
const isInit = ref<boolean>(false);
|
||||
|
||||
/**
|
||||
* chat图标点击事件
|
||||
*/
|
||||
function chatClick() {
|
||||
showChat.value = !showChat.value;
|
||||
if(showChat.value && !isInit.value){
|
||||
setTimeout(()=>{
|
||||
isInit.value = true;
|
||||
aiChatRef.value.initChat(appId.value);
|
||||
},100)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
let params: any = router.currentRoute.value.params;
|
||||
appId.value = params?.appId;
|
||||
isInit.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
left: unset;
|
||||
top: unset;
|
||||
|
||||
.footer-icon {
|
||||
cursor: pointer;
|
||||
background-color: #155eef;
|
||||
color: white;
|
||||
border-radius: 100%;
|
||||
padding: 20px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
|
||||
}
|
||||
.footer-close-icon {
|
||||
color: #0a3069;
|
||||
height: 48px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 20px;
|
||||
cursor: pointer;
|
||||
z-index: 9999;
|
||||
}
|
||||
.ai-chat {
|
||||
border: 1px solid #eeeeee;
|
||||
width: calc(100vh - 20px);
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
}
|
||||
</style>
|
919
jeecgboot-vue3/src/views/super/airag/aiapp/chat/chat.vue
Normal file
@ -0,0 +1,919 @@
|
||||
<template>
|
||||
<div class="chatWrap">
|
||||
<div class="content">
|
||||
<div class="header-title" v-if="type === 'view' && headerTitle">
|
||||
{{headerTitle}}
|
||||
</div>
|
||||
<div class="main">
|
||||
<div id="scrollRef" ref="scrollRef" class="scrollArea">
|
||||
<template v-if="chatData.length>0">
|
||||
<div class="chatContentArea">
|
||||
<chatMessage
|
||||
v-for="(item, index) of chatData"
|
||||
:key="index"
|
||||
:date-time="item.dateTime || item.datetime"
|
||||
:text="item.content"
|
||||
:inversion="item.inversion || item.role"
|
||||
:error="item.error"
|
||||
:loading="item.loading"
|
||||
:appData="appData"
|
||||
:presetQuestion="item.presetQuestion"
|
||||
:images = "item.images"
|
||||
@send="handleOutQuestion"
|
||||
></chatMessage>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="topArea">
|
||||
<presetQuestion @outQuestion="handleOutQuestion" :quickCommandData="quickCommandData"></presetQuestion>
|
||||
</div>
|
||||
<div class="bottomArea">
|
||||
<a-button type="text" class="delBtn" @click="handleDelSession()">
|
||||
<svg
|
||||
t="1706504908534"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="1584"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path
|
||||
d="M816.872727 158.254545h-181.527272V139.636364c0-39.563636-30.254545-69.818182-69.818182-69.818182h-107.054546c-39.563636 0-69.818182 30.254545-69.818182 69.818182v18.618181H207.127273c-48.872727 0-90.763636 41.890909-90.763637 93.09091s41.890909 90.763636 90.763637 90.763636h609.745454c51.2 0 90.763636-41.890909 90.763637-90.763636 0-51.2-41.890909-93.090909-90.763637-93.09091zM435.2 139.636364c0-13.963636 9.309091-23.272727 23.272727-23.272728h107.054546c13.963636 0 23.272727 9.309091 23.272727 23.272728v18.618181h-153.6V139.636364z m381.672727 155.927272H207.127273c-25.6 0-44.218182-20.945455-44.218182-44.218181 0-25.6 20.945455-44.218182 44.218182-44.218182h609.745454c25.6 0 44.218182 20.945455 44.218182 44.218182 0 23.272727-20.945455 44.218182-44.218182 44.218181zM835.490909 407.272727h-121.018182c-13.963636 0-23.272727 9.309091-23.272727 23.272728s9.309091 23.272727 23.272727 23.272727h97.745455V837.818182c0 39.563636-30.254545 69.818182-69.818182 69.818182h-37.236364V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-118.690909V602.763636c0-13.963636-9.309091-23.272727-23.272728-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364H372.363636V602.763636c0-13.963636-9.309091-23.272727-23.272727-23.272727s-23.272727 9.309091-23.272727 23.272727V907.636364h-34.909091c-39.563636 0-69.818182-30.254545-69.818182-69.818182V453.818182H558.545455c13.963636 0 23.272727-9.309091 23.272727-23.272727s-9.309091-23.272727-23.272727-23.272728H197.818182c-13.963636 0-23.272727 9.309091-23.272727 23.272728V837.818182c0 65.163636 51.2 116.363636 116.363636 116.363636h451.490909c65.163636 0 116.363636-51.2 116.363636-116.363636V430.545455c0-13.963636-11.636364-23.272727-23.272727-23.272728z"
|
||||
fill="currentColor"
|
||||
p-id="1585"
|
||||
></path>
|
||||
</svg>
|
||||
</a-button>
|
||||
<a-button v-if="type === 'view'" type="text" class="contextBtn" :class="[usingContext && 'enabled']" @click="handleUsingContext">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--ri"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.956 9.956 0 0 1-4.708-1.175L2 22l1.176-5.29A9.956 9.956 0 0 1 2 12C2 6.477 6.477 2 12 2m0 2a8 8 0 0 0-8 8c0 1.335.326 2.618.94 3.766l.35.654l-.656 2.946l2.948-.654l.653.349A7.955 7.955 0 0 0 12 20a8 8 0 1 0 0-16m1 3v5h4v2h-6V7z"
|
||||
></path>
|
||||
</svg>
|
||||
</a-button>
|
||||
<div class="chat-textarea" :class="textareaActive?'textarea-active':''">
|
||||
<div class="textarea-top" v-if="uploadUrlList && uploadUrlList.length>0">
|
||||
<div v-for="(item,index) in uploadUrlList" class="top-image" :key="index">
|
||||
<img :src="getImage(item)" @click="handlePreview(item)"/>
|
||||
<div class="upload-icon" @click="deleteImage(index)">
|
||||
<Icon icon="ant-design:close-outlined" size="12px"></Icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="textarea-bottom">
|
||||
<a-textarea
|
||||
ref="inputRef"
|
||||
v-model:value="prompt"
|
||||
:autoSize="{ minRows: 1, maxRows: 6 }"
|
||||
:placeholder="placeholder"
|
||||
@pressEnter="handleEnter"
|
||||
@focus="textareaActive = true"
|
||||
@blur="textareaActive = false"
|
||||
autofocus
|
||||
:readonly="loading"
|
||||
style="border-color: #ffffff !important;box-shadow:none"
|
||||
>
|
||||
</a-textarea>
|
||||
<a-button v-if="loading" type="primary" danger @click="handleStopChat" class="stopBtn">
|
||||
<svg
|
||||
t="1706148514627"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="5214"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path
|
||||
d="M512 967.111111c-250.311111 0-455.111111-204.8-455.111111-455.111111s204.8-455.111111 455.111111-455.111111 455.111111 204.8 455.111111 455.111111-204.8 455.111111-455.111111 455.111111z m0-56.888889c221.866667 0 398.222222-176.355556 398.222222-398.222222s-176.355556-398.222222-398.222222-398.222222-398.222222 176.355556-398.222222 398.222222 176.355556 398.222222 398.222222 398.222222z"
|
||||
fill="currentColor"
|
||||
p-id="5215"
|
||||
></path>
|
||||
<path d="M341.333333 341.333333h341.333334v341.333334H341.333333z" fill="currentColor" p-id="5216"></path>
|
||||
</svg>
|
||||
</a-button>
|
||||
<a-upload
|
||||
accept=".jpg,.jpeg,.png"
|
||||
v-if="!loading"
|
||||
name="file"
|
||||
v-model:file-list="fileInfoList"
|
||||
:showUploadList="false"
|
||||
:headers="headers"
|
||||
:beforeUpload="beforeUpload"
|
||||
@change="handleChange"
|
||||
:multiple="true"
|
||||
:action="uploadUrl"
|
||||
:max-count="3"
|
||||
>
|
||||
<a-tooltip title="图片上传,支持jpg/jpeg/png">
|
||||
<a-button class="sendBtn" type="text">
|
||||
<Icon icon="ant-design:picture-outlined" style="color: rgba(15,21,40,0.8)"></Icon>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</a-upload>
|
||||
<a-divider v-if="!loading" type="vertical" style="border-color:#38374314"></a-divider>
|
||||
<a-button
|
||||
@click="
|
||||
() => {
|
||||
handleSubmit();
|
||||
}
|
||||
"
|
||||
:disabled="!prompt"
|
||||
class="sendBtn"
|
||||
type="text"
|
||||
v-if="!loading"
|
||||
>
|
||||
<svg
|
||||
t="1706147858151"
|
||||
class="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="4237"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<path
|
||||
d="M865.28 202.5472c-17.1008-15.2576-41.0624-19.6608-62.5664-11.5712L177.7664 427.1104c-23.2448 8.8064-38.5024 29.696-39.6288 54.5792-1.1264 24.8832 11.9808 47.104 34.4064 58.0608l97.5872 47.7184c4.5056 2.2528 8.0896 6.0416 9.9328 10.6496l65.4336 161.1776c7.7824 19.1488 24.4736 32.9728 44.7488 37.0688 20.2752 4.096 41.0624-2.1504 55.6032-16.7936l36.352-36.352c6.4512-6.4512 16.5888-7.8848 24.576-3.3792l156.5696 88.8832c9.4208 5.3248 19.8656 8.0896 30.3104 8.0896 8.192 0 16.4864-1.6384 24.2688-5.0176 17.8176-7.68 30.72-22.8352 35.4304-41.6768l130.7648-527.1552c5.5296-22.016-1.7408-45.2608-18.8416-60.416z m-20.8896 50.7904L713.5232 780.4928c-1.536 6.2464-5.8368 11.3664-11.776 13.9264s-12.5952 2.1504-18.2272-1.024L526.9504 704.512c-9.4208-5.3248-19.8656-7.9872-30.208-7.9872-15.9744 0-31.744 6.144-43.52 17.92l-36.352 36.352c-3.8912 3.8912-8.9088 5.9392-14.2336 6.0416l55.6032-152.1664c0.512-1.3312 1.2288-2.56 2.2528-3.6864l240.3328-246.1696c8.2944-8.4992-2.048-21.9136-12.3904-16.0768L301.6704 559.8208c-4.096-3.584-8.704-6.656-13.6192-9.1136L190.464 502.9888c-11.264-5.5296-11.5712-16.1792-11.4688-19.3536 0.1024-3.1744 1.536-13.824 13.2096-18.2272L817.152 229.2736c10.4448-3.9936 18.0224 1.3312 20.8896 3.8912 2.8672 2.4576 9.0112 9.3184 6.3488 20.1728z"
|
||||
p-id="4238"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, watch } from 'vue';
|
||||
import { computed, ref, createVNode, onUnmounted, onMounted } from 'vue';
|
||||
import { useScroll } from './js/useScroll';
|
||||
import chatMessage from './chatMessage.vue';
|
||||
import presetQuestion from './presetQuestion.vue';
|
||||
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal, Tabs } from 'ant-design-vue';
|
||||
import './style/github-markdown.less';
|
||||
import './style/highlight.less';
|
||||
import './style/github-markdown.less';
|
||||
import dayjs from 'dayjs';
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import {getFileAccessHttpUrl, getHeaders} from "@/utils/common/compUtils";
|
||||
import { uploadUrl } from '/@/api/common/api';
|
||||
import { createImgPreview } from "@/components/Preview";
|
||||
|
||||
message.config({
|
||||
prefixCls: 'ai-chat-message',
|
||||
});
|
||||
|
||||
const props = defineProps(['uuid', 'prologue', 'formState', 'url', 'type','historyData','chatTitle','presetQuestion','quickCommandData']);
|
||||
const emit = defineEmits(['save','reload-message-title']);
|
||||
const { scrollRef, scrollToBottom } = useScroll();
|
||||
const prompt = ref<string>('');
|
||||
const loading = ref<boolean>(false);
|
||||
const inputRef = ref<Ref | null>(null);
|
||||
const headerTitle = ref<string>(props.chatTitle);
|
||||
|
||||
//聊天数据
|
||||
const chatData = ref<any>([]);
|
||||
//应用数据
|
||||
const appData = ref<any>({});
|
||||
const usingContext = ref<any>(true);
|
||||
const uuid = ref<string>(props.uuid);
|
||||
const topicId = ref<string>('');
|
||||
//请求id
|
||||
const requestId = ref<string>('');
|
||||
const conversationList = computed(() => chatData.value.filter((item) => item.inversion != 'user' && !!item.conversationOptions));
|
||||
const placeholder = computed(() => {
|
||||
return '来说点什么吧...(Shift + Enter = 换行)';
|
||||
});
|
||||
//token
|
||||
const headers = getHeaders();
|
||||
//文本域点击事件
|
||||
const textareaActive = ref<boolean>(false);
|
||||
|
||||
function handleEnter(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
function handleSubmit() {
|
||||
let message = prompt.value;
|
||||
if (!message || message.trim() === '') return;
|
||||
prompt.value = '';
|
||||
onConversation(message);
|
||||
}
|
||||
const handleOutQuestion = (message) => {
|
||||
onConversation(message);
|
||||
};
|
||||
async function onConversation(message) {
|
||||
if(!props.type && props.type != 'view'){
|
||||
if(appData.value.type && appData.value.type == 'chatSimple' && !appData.value.modelId) {
|
||||
messageTip("请选择AI模型");
|
||||
return;
|
||||
}
|
||||
if(appData.value.type && appData.value.type == 'chatFLow' && !appData.value.flowId) {
|
||||
messageTip("请选择关联流程");
|
||||
return;
|
||||
}
|
||||
if(!appData.value.name) {
|
||||
messageTip("请填写应用名称");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
|
||||
addChat(uuid.value, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: message,
|
||||
images:uploadUrlList.value?uploadUrlList.value:[],
|
||||
inversion: 'user',
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: null },
|
||||
});
|
||||
scrollToBottom();
|
||||
|
||||
let options: any = {};
|
||||
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions;
|
||||
if (lastContext && usingContext.value) {
|
||||
options = { ...lastContext };
|
||||
}
|
||||
|
||||
addChat(uuid.value, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: '思考中...',
|
||||
loading: true,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
});
|
||||
|
||||
scrollToBottom();
|
||||
|
||||
//发送消息
|
||||
sendMessage(message,options);
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
updateChatSome(uuid.value, chatData.value.length - 1, { loading: false });
|
||||
});
|
||||
|
||||
const addChat = (uuid, data) => {
|
||||
chatData.value.push({ ...data });
|
||||
};
|
||||
const updateChat = async (uuid, index, data) => {
|
||||
chatData.value.splice(index, 1, data);
|
||||
await scrollToBottom();
|
||||
};
|
||||
/**
|
||||
* 顶置开场白
|
||||
* @param txt
|
||||
*/
|
||||
const topChat = (txt) => {
|
||||
let data = {
|
||||
content: txt,
|
||||
key: 'prologue',
|
||||
loading: false,
|
||||
dateTime: dayjs().format('YYYY/MM/DD HH:mm:ss'),
|
||||
inversion: 'ai',
|
||||
presetQuestion: props.presetQuestion ? JSON.parse(props.presetQuestion) : "",
|
||||
};
|
||||
if (chatData.value && chatData.value.length > 0) {
|
||||
let key = chatData.value[0].key;
|
||||
if (key === 'prologue') {
|
||||
chatData.value[0] = { ...data };
|
||||
return;
|
||||
}
|
||||
}
|
||||
chatData.value.unshift({ ...data });
|
||||
};
|
||||
const updateChatSome = (uuid, index, data) => {
|
||||
chatData.value[index] = { ...chatData.value[index], ...data };
|
||||
};
|
||||
const updateChatFail = (uuid, index, data) => {
|
||||
updateChat(uuid.value, chatData.value.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: data,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: null,
|
||||
requestOptions: null,
|
||||
});
|
||||
scrollToBottom();
|
||||
};
|
||||
|
||||
/**
|
||||
* 清空会话
|
||||
* @param id
|
||||
*/
|
||||
function handleDelSession (){
|
||||
Modal.confirm({
|
||||
title: '清空会话',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
content: '是否清空会话?',
|
||||
closable: true,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
wrapClassName:'ai-chat-modal',
|
||||
async onOk() {
|
||||
try {
|
||||
defHttp.get({
|
||||
url: '/airag/chat/messages/clear/' + uuid.value,
|
||||
},{ isTransformResponse: false }).then((res) => {
|
||||
if(res.success){
|
||||
chatData.value = [];
|
||||
topicId.value = "";
|
||||
if(props.prologue){
|
||||
topChat(props.prologue);
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
return console.log('Oops errors!');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 停止响应
|
||||
const handleStop = () => {
|
||||
console.log('ai 聊天:::---停止响应');
|
||||
if (loading.value) {
|
||||
loading.value = false;
|
||||
}
|
||||
updateChatSome(uuid, chatData.value.length - 1, { loading: false });
|
||||
};
|
||||
|
||||
handleStop();
|
||||
|
||||
/**
|
||||
* 停止消息
|
||||
*/
|
||||
function handleStopChat() {
|
||||
if(requestId.value){
|
||||
//调用后端接口停止响应
|
||||
defHttp.get({
|
||||
url: '/airag/chat/stop/' + requestId.value,
|
||||
},{ isTransformResponse: false });
|
||||
}
|
||||
handleStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文本
|
||||
* @param message
|
||||
* @param options
|
||||
*/
|
||||
async function sendMessage(message, options) {
|
||||
let param = {};
|
||||
if (!props.type && props.type != 'view') {
|
||||
param = {
|
||||
content: message,
|
||||
images: uploadUrlList.value?uploadUrlList.value:[],
|
||||
topicId: topicId.value,
|
||||
app: appData.value,
|
||||
responseMode: 'streaming',
|
||||
};
|
||||
}else{
|
||||
param = {
|
||||
content: message,
|
||||
topicId: usingContext.value?topicId.value:'',
|
||||
images: uploadUrlList.value?uploadUrlList.value:[],
|
||||
appId: appData.value.id,
|
||||
responseMode: 'streaming',
|
||||
conversationId: uuid.value === "1002"?'':uuid.value
|
||||
};
|
||||
|
||||
if(headerTitle.value == '新建聊天'){
|
||||
headerTitle.value = message.length>5?message.substring(0,5):message
|
||||
}
|
||||
|
||||
emit("reload-message-title",message.length>5?message.substring(0,5):message)
|
||||
}
|
||||
|
||||
uploadUrlList.value = [];
|
||||
fileInfoList.value = [];
|
||||
|
||||
const readableStream = await defHttp.post(
|
||||
{
|
||||
url: props.url,
|
||||
params: param,
|
||||
adapter: 'fetch',
|
||||
responseType: 'stream',
|
||||
timeout: 5 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
isTransformResponse: false,
|
||||
}
|
||||
).catch((e)=>{
|
||||
updateChatFail(uuid, chatData.value.length - 1, "服务器错误,请稍后重试!");
|
||||
handleStop();
|
||||
return;
|
||||
});
|
||||
const reader = readableStream.getReader();
|
||||
const decoder = new TextDecoder('UTF-8');
|
||||
let conversationId = '';
|
||||
let buffer = '';
|
||||
let text = ''; // 按 SSE 协议分割消息
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
|
||||
let result = decoder.decode(value, { stream: true });
|
||||
result = buffer + result;
|
||||
const lines = result.split('\n\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
const content = line.replace('data:', '').trim();
|
||||
if(!content){
|
||||
continue;
|
||||
}
|
||||
if(!content.endsWith('}')){
|
||||
buffer = buffer + line;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
try {
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
let parse = JSON.parse(content);
|
||||
await renderText(parse,conversationId,text,options).then((res)=>{
|
||||
text = res.returnText;
|
||||
conversationId = res.conversationId;
|
||||
});
|
||||
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
} catch (error) {
|
||||
console.log('Error parsing update:', error);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-12---for:【QQYUN-11555】聊天时要流式显示消息---
|
||||
}else{
|
||||
if(!line){
|
||||
continue;
|
||||
}
|
||||
if(!line.endsWith('}')){
|
||||
buffer = buffer + line;
|
||||
continue;
|
||||
}
|
||||
buffer = "";
|
||||
//update-begin---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
try {
|
||||
let parse = JSON.parse(line);
|
||||
await renderText(parse, conversationId, text, options).then((res) => {
|
||||
text = res.returnText;
|
||||
conversationId = res.conversationId;
|
||||
});
|
||||
}catch (error) {
|
||||
console.log('Error parsing update:', error);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-13---for:【QQYUN-11572】发布到线上不能实时动态,内容不能加载出来,得刷新才能看到全部回答---
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 是否使用上下文
|
||||
const handleUsingContext = () => {
|
||||
usingContext.value = !usingContext.value;
|
||||
if (usingContext.value) {
|
||||
message.success("当前模式下, 发送消息会携带之前的聊天记录");
|
||||
} else {
|
||||
message.warning("当前模式下, 发送消息不会携带之前的聊天记录");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 提示
|
||||
* @param value
|
||||
*/
|
||||
function messageTip(value) {
|
||||
message.warning(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染文本
|
||||
* @param item
|
||||
* @param conversationId
|
||||
* @param text
|
||||
* @param options
|
||||
*/
|
||||
async function renderText(item,conversationId,text,options) {
|
||||
let returnText = "";
|
||||
if (item.event == 'MESSAGE') {
|
||||
text = text + item.data.message;
|
||||
returnText = text;
|
||||
//更新聊天信息
|
||||
updateChat(uuid.value, chatData.value.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: text,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: { conversationId: conversationId, parentMessageId: topicId.value },
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
});
|
||||
}
|
||||
if (item.event == 'MESSAGE_END') {
|
||||
topicId.value = item.topicId;
|
||||
conversationId = item.conversationId;
|
||||
uuid.value = item.conversationId;
|
||||
requestId.value = item.requestId;
|
||||
handleStop();
|
||||
}
|
||||
if (item.event == 'FLOW_FINISHED') {
|
||||
//update-begin---author:wangshuai---date:2025-03-07---for:【QQYUN-11457】聊天调用流程,执行失败了但是没提示---
|
||||
if(item.data && !item.data.success){
|
||||
updateChatFail(uuid, chatData.value.length - 1, item.data.message?item.data.message:'请求出错,请稍后重试!');
|
||||
handleStop();
|
||||
return "";
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-07---for:【QQYUN-11457】聊天调用流程,执行失败了但是没提示---
|
||||
topicId.value = item.topicId;
|
||||
conversationId = item.conversationId;
|
||||
uuid.value = item.conversationId;
|
||||
requestId.value = item.requestId;
|
||||
handleStop();
|
||||
}
|
||||
if (item.event == 'ERROR') {
|
||||
updateChatFail(uuid, chatData.value.length - 1, item.data.message?item.data.message:'请求出错,请稍后重试!');
|
||||
handleStop();
|
||||
return "";
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-03-21---for:【QQYUN-11495】【AI】实时展示当前思考进度---
|
||||
if(item.event === "NODE_STARTED"){
|
||||
let aiText = "";
|
||||
if(item.data.type === 'llm'){
|
||||
aiText = "正在构建响应内容";
|
||||
}
|
||||
if(item.data.type === 'knowledge'){
|
||||
aiText = "正在对知识库进行深度检索";
|
||||
}
|
||||
if(item.data.type === 'classifier'){
|
||||
aiText = "正在分类";
|
||||
}
|
||||
if(item.data.type === 'code'){
|
||||
aiText = "正在实施代码运行操作";
|
||||
}
|
||||
if(item.data.type === 'subflow'){
|
||||
aiText = "正在运行子流程";
|
||||
}
|
||||
if(item.data.type === 'enhanceJava'){
|
||||
aiText = "正在执行java增强";
|
||||
}
|
||||
if(item.data.type === 'http'){
|
||||
aiText = "正在发送http请求";
|
||||
}
|
||||
//更新聊天信息
|
||||
updateChat(uuid.value, chatData.value.length - 1, {
|
||||
dateTime: new Date().toLocaleString(),
|
||||
content: aiText,
|
||||
inversion: 'ai',
|
||||
error: false,
|
||||
loading: true,
|
||||
conversationOptions: null,
|
||||
requestOptions: { prompt: message, options: { ...options } },
|
||||
});
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-21---for:【QQYUN-11495】【AI】实时展示当前思考进度---
|
||||
|
||||
return { returnText, conversationId };
|
||||
}
|
||||
|
||||
//上传文件列表集合
|
||||
const uploadUrlList = ref<any>([]);
|
||||
//文件集合
|
||||
const fileInfoList = ref<any>([]);
|
||||
|
||||
/**
|
||||
* 文件上传回调事件
|
||||
* @param info
|
||||
*/
|
||||
function handleChange(info) {
|
||||
let { fileList, file } = info;
|
||||
fileInfoList.value = fileList;
|
||||
if (file.status === 'error') {
|
||||
message.error(file.response?.message || `${file.name} 上传失败,请查看服务端日志`);
|
||||
}
|
||||
if (file.status === 'done') {
|
||||
uploadUrlList.value.push(file.response.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片地址
|
||||
*
|
||||
* @param url
|
||||
*/
|
||||
function getImage(url) {
|
||||
return getFileAccessHttpUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前事件
|
||||
*/
|
||||
function beforeUpload(file) {
|
||||
var fileType = file.type;
|
||||
if (fileType === 'image') {
|
||||
if (fileType.indexOf('image') < 0) {
|
||||
message.warning('请上传图片');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
*/
|
||||
function deleteImage(index) {
|
||||
uploadUrlList.value.splice(index,1);
|
||||
fileInfoList.value.splice(index,1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片预览
|
||||
* @param url
|
||||
*/
|
||||
function handlePreview(url){
|
||||
const onImgLoad = ({ index, url, dom }) => {
|
||||
console.log(`第${index + 1}张图片已加载,URL为:${url}`, dom);
|
||||
};
|
||||
let imageList = [getImage(url)];
|
||||
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
|
||||
}
|
||||
|
||||
//监听开场白
|
||||
watch(
|
||||
() => props.prologue,
|
||||
(val) => {
|
||||
try {
|
||||
if (val) {
|
||||
topChat(val);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
);
|
||||
|
||||
//监听开场白预制问题
|
||||
watch(
|
||||
() => props.presetQuestion,
|
||||
(val) => {
|
||||
topChat(props.prologue);
|
||||
}
|
||||
);
|
||||
|
||||
//监听应用信息
|
||||
watch(
|
||||
() => props.formState,
|
||||
(val) => {
|
||||
try {
|
||||
if (val) {
|
||||
appData.value = val;
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
//监听历史信息
|
||||
watch(
|
||||
() => props.historyData,
|
||||
(val) => {
|
||||
try {
|
||||
//update-begin---author:wangshuai---date:2025-03-06---for:【QQYUN-11384】浏览器打开应用开场白丢了---
|
||||
if (val && val.length > 0) {
|
||||
chatData.value = cloneDeep(val);
|
||||
if(chatData.value[0]){
|
||||
topicId.value = chatData.value[0].topicId
|
||||
}
|
||||
}else{
|
||||
chatData.value = [];
|
||||
headerTitle.value = props.chatTitle;
|
||||
}
|
||||
if(props.prologue && props.chatTitle){
|
||||
topChat(props.prologue)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-06---for:【QQYUN-11384】浏览器打开应用开场白丢了---
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
scrollToBottom();
|
||||
uploadUrlList.value = [];
|
||||
fileInfoList.value = [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chatWrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
.content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
.scrollArea {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
.chatContentArea {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
.emptyArea {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
.stopArea {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 6px 16px;
|
||||
.topArea {
|
||||
padding-left: 6%;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.bottomArea {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ant-input {
|
||||
margin: 0 8px;
|
||||
}
|
||||
.ant-input,
|
||||
.ant-btn {
|
||||
height: 36px;
|
||||
}
|
||||
textarea.ant-input {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.contextBtn,
|
||||
.delBtn {
|
||||
padding: 0;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.delBtn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.contextBtn {
|
||||
color: #a8071a;
|
||||
&.enabled {
|
||||
color: @primary-color;
|
||||
}
|
||||
font-size: 18px;
|
||||
}
|
||||
.sendBtn {
|
||||
font-size: 18px;
|
||||
width: 36px;
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.stopBtn {
|
||||
width: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.chatgpt .markdown-body) {
|
||||
background-color: #f4f6f8;
|
||||
}
|
||||
:deep(.ant-message) {
|
||||
top: 50% !important;
|
||||
}
|
||||
.header-title{
|
||||
color: #101828;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
padding-bottom: 8px;
|
||||
margin-left: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chat-textarea{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border-radius: 15px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
flex-direction: column;
|
||||
transition: width 0.3s;
|
||||
border-color: rgba(68,83,130,0.2);
|
||||
.textarea-top{
|
||||
border-bottom: 1px solid #f0f0f5;
|
||||
padding: 12px 28px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
.top-image{
|
||||
display: flex;
|
||||
img{
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.textarea-bottom{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1 1;
|
||||
min-height: 48px;
|
||||
position: relative;
|
||||
padding: 8px 8px 8px 10px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.chat-textarea:hover{
|
||||
border-color: rgba(59,130,246,0.5)
|
||||
}
|
||||
.textarea-active{
|
||||
border-color: rgba(59,130,246,0.5) !important;
|
||||
}
|
||||
:deep(.ant-divider-vertical){
|
||||
margin: 0 2px;
|
||||
}
|
||||
.upload-icon{
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
background-color: #1D1C23;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-left: 44px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
.top-image:hover{
|
||||
.upload-icon{
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.ai-chat-modal{
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
.ai-chat-message{
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
</style>
|
157
jeecgboot-vue3/src/views/super/airag/aiapp/chat/chatMessage.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="chat" :class="[inversion === 'user' ? 'self' : 'chatgpt']">
|
||||
<div class="avatar">
|
||||
<img v-if="inversion === 'user'" :src="avatar()" />
|
||||
<img v-else :src="getAiImg()">
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="date">
|
||||
<span v-if="inversion === 'ai'" style="margin-right: 10px">{{appData.name || 'AI助手'}}</span>
|
||||
<span>{{ dateTime }}</span>
|
||||
</p>
|
||||
<div v-if="inversion === 'user' && images && images.length>0" class="images">
|
||||
<div v-for="(item,index) in images" :key="index" class="image" @click="handlePreview(item)">
|
||||
<img :src="getImageUrl(item)"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msgArea">
|
||||
<chatText :text="text" :inversion="inversion" :error="error" :loading="loading"></chatText>
|
||||
</div>
|
||||
<div v-if="presetQuestion" v-for="item in presetQuestion" class="question" @click="presetQuestionClick(item.descr)">
|
||||
<span>{{item.descr}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import chatText from './chatText.vue';
|
||||
import defaultAvatar from '/@/assets/images/ai/avatar.jpg';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import defaultImg from '../img/ailogo.png';
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading','appData','presetQuestion','images']);
|
||||
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
|
||||
import { createImgPreview } from "@/components/Preview";
|
||||
const { userInfo } = useUserStore();
|
||||
const avatar = () => {
|
||||
return getFileAccessHttpUrl(userInfo?.avatar) || defaultAvatar;
|
||||
};
|
||||
const emit = defineEmits(['send']);
|
||||
const getAiImg = () => {
|
||||
return getFileAccessHttpUrl(props.appData?.icon) || defaultImg;
|
||||
};
|
||||
|
||||
/**
|
||||
* 预设问题点击事件
|
||||
*
|
||||
*/
|
||||
function presetQuestionClick(descr) {
|
||||
emit("send",descr)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
function getImageUrl(item) {
|
||||
let url = item;
|
||||
if(item.hasOwnProperty('url')){
|
||||
url = item.url;
|
||||
}
|
||||
if(item.hasOwnProperty('base64Data') && item.base64Data){
|
||||
return item.base64Data;
|
||||
}
|
||||
return getFileAccessHttpUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片预览
|
||||
* @param url
|
||||
*/
|
||||
function handlePreview(url){
|
||||
const onImgLoad = ({ index, url, dom }) => {
|
||||
console.log(`第${index + 1}张图片已加载,URL为:${url}`, dom);
|
||||
};
|
||||
let imageList = [getImageUrl(url)];
|
||||
createImgPreview({ imageList: imageList, defaultWidth: 700, rememberState: true, onImgLoad });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chat {
|
||||
display: flex;
|
||||
margin-bottom: 1.5rem;
|
||||
&.self {
|
||||
flex-direction: row-reverse;
|
||||
.avatar {
|
||||
margin-right: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.msgArea {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.date {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
flex: none;
|
||||
margin-right: 10px;
|
||||
img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
svg {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
.date {
|
||||
color: #b4bbc4;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.msgArea {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.question{
|
||||
margin-top: 10px;
|
||||
border-radius: 0.375rem;
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
background-color: #ffffff;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.images{
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
.image{
|
||||
width: 120px;
|
||||
height: 80px;
|
||||
cursor: pointer;
|
||||
img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="textWrap" :class="[inversion ? 'self' : 'chatgpt']" ref="textRef">
|
||||
<div v-if="!inversion">
|
||||
<div v-if="text != ''" class="textWrap" :class="[inversion === 'user' ? 'self' : 'chatgpt']" ref="textRef">
|
||||
<div v-if="inversion != 'user'">
|
||||
<div class="markdown-body" :class="{ 'markdown-body-generate': loading }" v-html="text" />
|
||||
</div>
|
||||
<div v-else class="msg" v-text="text" />
|
||||
@ -13,6 +13,9 @@
|
||||
import mdKatex from '@traptitech/markdown-it-katex';
|
||||
import mila from 'markdown-it-link-attributes';
|
||||
import hljs from 'highlight.js';
|
||||
import './style/github-markdown.less';
|
||||
import './style/highlight.less';
|
||||
import './style/style.less';
|
||||
|
||||
const props = defineProps(['dateTime', 'text', 'inversion', 'error', 'loading']);
|
||||
const textRef = ref();
|
||||
@ -34,7 +37,7 @@
|
||||
|
||||
const text = computed(() => {
|
||||
const value = props.text ?? '';
|
||||
if (!props.inversion) return mdi.render(value);
|
||||
if (props.inversion != 'user') return mdi.render(value);
|
||||
return value;
|
||||
});
|
||||
|
@ -1,28 +1,28 @@
|
||||
import { useChatStore } from '@/store'
|
||||
import { useChatStore } from '@/store';
|
||||
|
||||
export function useChat() {
|
||||
const chatStore = useChatStore()
|
||||
const chatStore = useChatStore();
|
||||
|
||||
const getChatByUuidAndIndex = (uuid: number, index: number) => {
|
||||
return chatStore.getChatByUuidAndIndex(uuid, index)
|
||||
}
|
||||
return chatStore.getChatByUuidAndIndex(uuid, index);
|
||||
};
|
||||
|
||||
const addChat = (uuid: number, chat: Chat.Chat) => {
|
||||
chatStore.addChatByUuid(uuid, chat)
|
||||
}
|
||||
chatStore.addChatByUuid(uuid, chat);
|
||||
};
|
||||
|
||||
const updateChat = (uuid: number, index: number, chat: Chat.Chat) => {
|
||||
chatStore.updateChatByUuid(uuid, index, chat)
|
||||
}
|
||||
chatStore.updateChatByUuid(uuid, index, chat);
|
||||
};
|
||||
|
||||
const updateChatSome = (uuid: number, index: number, chat: Partial<Chat.Chat>) => {
|
||||
chatStore.updateChatSomeByUuid(uuid, index, chat)
|
||||
}
|
||||
chatStore.updateChatSomeByUuid(uuid, index, chat);
|
||||
};
|
||||
|
||||
return {
|
||||
addChat,
|
||||
updateChat,
|
||||
updateChatSome,
|
||||
getChatByUuidAndIndex,
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import type { Ref } from 'vue';
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
type ScrollElement = HTMLDivElement | null;
|
||||
|
||||
interface ScrollReturn {
|
||||
scrollRef: Ref<ScrollElement>;
|
||||
scrollToBottom: () => Promise<void>;
|
||||
scrollToTop: () => Promise<void>;
|
||||
scrollToBottomIfAtBottom: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useScroll(): ScrollReturn {
|
||||
const scrollRef = ref<ScrollElement>(null);
|
||||
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick();
|
||||
if (scrollRef.value) scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
|
||||
};
|
||||
|
||||
const scrollToTop = async () => {
|
||||
await nextTick();
|
||||
if (scrollRef.value) scrollRef.value.scrollTop = 0;
|
||||
};
|
||||
|
||||
const scrollToBottomIfAtBottom = async () => {
|
||||
await nextTick();
|
||||
if (scrollRef.value) {
|
||||
const threshold = 100; // Threshold, indicating the distance threshold to the bottom of the scroll bar.
|
||||
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight;
|
||||
if (distanceToBottom <= threshold) scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
scrollToBottom,
|
||||
scrollToTop,
|
||||
scrollToBottomIfAtBottom,
|
||||
};
|
||||
}
|
159
jeecgboot-vue3/src/views/super/airag/aiapp/chat/js/chat.js
Normal file
@ -0,0 +1,159 @@
|
||||
// iframe-widget.js
|
||||
(function () {
|
||||
let widgetInstance = null;
|
||||
const defaultConfig = {
|
||||
// 支持'top-left'左上, 'top-right'右上, 'bottom-left'左下, 'bottom-right'右下
|
||||
iconPosition: 'bottom-right',
|
||||
//图标的大小
|
||||
iconSize: '30px',
|
||||
//图标的颜色
|
||||
iconColor: '#155eef',
|
||||
//必填不允许修改
|
||||
appId: '',
|
||||
//聊天弹窗的宽度
|
||||
chatWidth: '800px',
|
||||
//聊天弹窗的高度
|
||||
chatHeight: '700px',
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建ai图标
|
||||
* @param config
|
||||
*/
|
||||
function createAiChat(config) {
|
||||
// 单例模式,确保只存在一个实例
|
||||
if (widgetInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 合并配置
|
||||
const finalConfig = { ...defaultConfig, ...config };
|
||||
|
||||
if (!finalConfig.appId) {
|
||||
console.error('appId为空!');
|
||||
return;
|
||||
}
|
||||
// 创建容器
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 998;
|
||||
${getPositionStyles(finalConfig.iconPosition)}
|
||||
cursor: pointer;
|
||||
`;
|
||||
// 创建图标
|
||||
const icon = document.createElement('div');
|
||||
icon.style.cssText = `
|
||||
width: ${finalConfig.iconSize};
|
||||
height: ${finalConfig.iconSize};
|
||||
background-color: ${finalConfig.iconColor};
|
||||
border-radius: 50%;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 4px 8px 0;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
`;
|
||||
icon.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 1024 1024" class="iconify iconify--ant-design"><path fill="currentColor" d="M573 421c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40m-280 0c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40"></path><path fill="currentColor" d="M894 345c-48.1-66-115.3-110.1-189-130v.1c-17.1-19-36.4-36.5-58-52.1c-163.7-119-393.5-82.7-513 81c-96.3 133-92.2 311.9 6 439l.8 132.6c0 3.2.5 6.4 1.5 9.4c5.3 16.9 23.3 26.2 40.1 20.9L309 806c33.5 11.9 68.1 18.7 102.5 20.6l-.5.4c89.1 64.9 205.9 84.4 313 49l127.1 41.4c3.2 1 6.5 1.6 9.9 1.6c17.7 0 32-14.3 32-32V753c88.1-119.6 90.4-284.9 1-408M323 735l-12-5l-99 31l-1-104l-8-9c-84.6-103.2-90.2-251.9-11-361c96.4-132.2 281.2-161.4 413-66c132.2 96.1 161.5 280.6 66 412c-80.1 109.9-223.5 150.5-348 102m505-17l-8 10l1 104l-98-33l-12 5c-56 20.8-115.7 22.5-171 7l-.2-.1C613.7 788.2 680.7 742.2 729 676c76.4-105.3 88.8-237.6 44.4-350.4l.6.4c23 16.5 44.1 37.1 62 62c72.6 99.6 68.5 235.2-8 330"></path><path fill="currentColor" d="M433 421c-23.1 0-41 17.9-41 40s17.9 40 41 40c21.1 0 39-17.9 39-40s-17.9-40-39-40"></path></svg>';
|
||||
|
||||
// 创建iframe容器
|
||||
const iframeContainer = document.createElement('div');
|
||||
iframeContainer.style.cssText = `
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
width: ${finalConfig.chatWidth} !important;
|
||||
height: ${finalConfig.chatHeight} !important;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.2);
|
||||
display: none;
|
||||
z-index: 10000;
|
||||
`;
|
||||
|
||||
// 创建iframe
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
iframe.id = 'ai-app-chat-document';
|
||||
iframe.src = getIframeSrc(finalConfig) + '/ai/app/chat/' + finalConfig.appId;
|
||||
// 创建关闭按钮
|
||||
const closeBtn = document.createElement('div');
|
||||
closeBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 1024 1024" class="iconify iconify--ant-design"><path fill="currentColor" fill-rule="evenodd" d="M799.855 166.312c.023.007.043.018.084.059l57.69 57.69c.041.041.052.06.059.084a.1.1 0 0 1 0 .069c-.007.023-.018.042-.059.083L569.926 512l287.703 287.703c.041.04.052.06.059.083a.12.12 0 0 1 0 .07c-.007.022-.018.042-.059.083l-57.69 57.69c-.041.041-.06.052-.084.059a.1.1 0 0 1-.069 0c-.023-.007-.042-.018-.083-.059L512 569.926L224.297 857.629c-.04.041-.06.052-.083.059a.12.12 0 0 1-.07 0c-.022-.007-.042-.018-.083-.059l-57.69-57.69c-.041-.041-.052-.06-.059-.084a.1.1 0 0 1 0-.069c.007-.023.018-.042.059-.083L454.073 512L166.371 224.297c-.041-.04-.052-.06-.059-.083a.12.12 0 0 1 0-.07c.007-.022.018-.042.059-.083l57.69-57.69c.041-.041.06-.052.084-.059a.1.1 0 0 1 .069 0c.023.007.042.018.083.059L512 454.073l287.703-287.702c.04-.041.06-.052.083-.059a.12.12 0 0 1 .07 0Z"></path></svg>';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: -10px;
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
`;
|
||||
|
||||
// 组装元素
|
||||
iframeContainer.appendChild(closeBtn);
|
||||
iframeContainer.appendChild(iframe);
|
||||
document.body.appendChild(iframeContainer);
|
||||
container.appendChild(icon);
|
||||
document.body.appendChild(container);
|
||||
|
||||
// 事件监听
|
||||
icon.addEventListener('click', () => {
|
||||
iframeContainer.style.display = 'block';
|
||||
});
|
||||
|
||||
closeBtn.addEventListener('click', () => {
|
||||
iframeContainer.style.display = 'none';
|
||||
});
|
||||
|
||||
// 保存实例引用
|
||||
widgetInstance = {
|
||||
remove: () => {
|
||||
container.remove();
|
||||
iframeContainer.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取位置信息
|
||||
*
|
||||
* @param position
|
||||
* @returns {*|string}
|
||||
*/
|
||||
function getPositionStyles(position) {
|
||||
const positions = {
|
||||
'top-left': 'top: 20px; left: 20px;',
|
||||
'top-right': 'top: 20px; right: 20px;',
|
||||
'bottom-left': 'bottom: 20px; left: 20px;',
|
||||
'bottom-right': 'bottom: 20px; right: 20px;',
|
||||
};
|
||||
return positions[position] || positions['bottom-right'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取src地址
|
||||
*/
|
||||
function getIframeSrc(finalConfig) {
|
||||
const specificScript = document.getElementById("e7e007dd52f67fe36365eff636bbffbd");
|
||||
if (specificScript) {
|
||||
return specificScript.src.substring(0, specificScript.src.indexOf('/', specificScript.src.indexOf('://') + 3));
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露全局方法
|
||||
window.createAiChat = createAiChat;
|
||||
})();
|
@ -0,0 +1,41 @@
|
||||
import type { Ref } from 'vue';
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
type ScrollElement = HTMLDivElement | null;
|
||||
|
||||
interface ScrollReturn {
|
||||
scrollRef: Ref<ScrollElement>;
|
||||
scrollToBottom: () => Promise<void>;
|
||||
scrollToTop: () => Promise<void>;
|
||||
scrollToBottomIfAtBottom: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useScroll(): ScrollReturn {
|
||||
const scrollRef = ref<ScrollElement>(null);
|
||||
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick();
|
||||
if (scrollRef.value) scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
|
||||
};
|
||||
|
||||
const scrollToTop = async () => {
|
||||
await nextTick();
|
||||
if (scrollRef.value) scrollRef.value.scrollTop = 0;
|
||||
};
|
||||
|
||||
const scrollToBottomIfAtBottom = async () => {
|
||||
await nextTick();
|
||||
if (scrollRef.value) {
|
||||
const threshold = 100; // Threshold, indicating the distance threshold to the bottom of the scroll bar.
|
||||
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight;
|
||||
if (distanceToBottom <= threshold) scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
scrollToBottom,
|
||||
scrollToTop,
|
||||
scrollToBottomIfAtBottom,
|
||||
};
|
||||
}
|
@ -19,7 +19,13 @@
|
||||
</svg>
|
||||
<div class="content">
|
||||
<ul ref="ulElemRef">
|
||||
<li v-for="(item, index) in data" :key="index" class="item" @click="handleQuestion(item)">{{ item }}</li>
|
||||
<li v-for="(item, index) in data" :key="index" class="item" @click="handleQuestion(item.descr)">
|
||||
<div class="question-descr">
|
||||
<Icon v-if="item.icon" :icon="item.icon" size="20"></Icon>
|
||||
<svg v-else width="14px" height="14px" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"></path></svg>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<svg
|
||||
@ -43,9 +49,12 @@
|
||||
</template>
|
||||
|
||||
<script name="presetQuestion" setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import {ref, onMounted, onBeforeUnmount, watch} from 'vue';
|
||||
const emit = defineEmits(['outQuestion']);
|
||||
const data = ref(['请介绍一下JeecgBoot', 'JEECG有哪些优势?', 'JEECG可以做哪些事情?']);
|
||||
const props = defineProps({
|
||||
quickCommandData:{ type: Object },
|
||||
});
|
||||
const data = ref(props.quickCommandData);
|
||||
const leftBtnStatus = ref('');
|
||||
const rightBtnStatus = ref('');
|
||||
const rightBtn = ref('');
|
||||
@ -84,6 +93,11 @@
|
||||
const handleQuestion = (item) => {
|
||||
emit('outQuestion', item);
|
||||
};
|
||||
|
||||
watch(()=>props.quickCommandData, (val) => {
|
||||
data.value = props.quickCommandData;
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
ulElemRef.value.addEventListener('scroll', handleScroll, false);
|
||||
handleScroll({ target: ulElemRef.value });
|
||||
@ -102,7 +116,7 @@
|
||||
height: 14px;
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
color: #c6c2c2;
|
||||
&.leftBtn {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@ -147,5 +161,12 @@
|
||||
border-color: @primary-color;
|
||||
}
|
||||
}
|
||||
.question-descr{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
span{
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,65 @@
|
||||
import type { App } from 'vue';
|
||||
import { router } from "/@/router";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
import { LAYOUT } from "@/router/constant";
|
||||
|
||||
const ChatRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/ai/app/chat/:appId",
|
||||
name: "ai-chat-@appId-@modeType",
|
||||
component: () => import("/@/views/super/airag/aiapp/chat/AiChat.vue"),
|
||||
meta: {
|
||||
title: 'AI聊天',
|
||||
ignoreAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/ai/app/chatIcon/:appId",
|
||||
name: "ai-chatIcon-@appId",
|
||||
component: () => import("/@/views/super/airag/aiapp/chat/AiChatIcon.vue"),
|
||||
meta: {
|
||||
title: 'AI聊天',
|
||||
ignoreAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/ai/chat',
|
||||
name: 'aiChat',
|
||||
component: LAYOUT,
|
||||
meta: {
|
||||
title: 'ai聊天',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/ai/chat/:appId",
|
||||
name: "ai-chat-@appId",
|
||||
component: () => import("/@/views/super/airag/aiapp/chat/AiChat.vue"),
|
||||
meta: {
|
||||
title:'AI助手',
|
||||
ignoreAuth: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/ai/chat",
|
||||
name: "ai-chat",
|
||||
component: () => import("/@/views/super/airag/aiapp/chat/AiChat.vue"),
|
||||
meta: {
|
||||
title:'AI助手',
|
||||
ignoreAuth: false,
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/** 注册路由 */
|
||||
export async function register(app: App) {
|
||||
await registerMyAppRouter(app);
|
||||
console.log('[聊天路由] 注册完成!');
|
||||
}
|
||||
|
||||
async function registerMyAppRouter(_: App) {
|
||||
for(let appRoute of ChatRoutes){
|
||||
await router.addRoute(appRoute);
|
||||
}
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
<template>
|
||||
<div class="slide-wrap">
|
||||
<div class="header">
|
||||
<img class="header-image" :src="getImage()" />
|
||||
<div class="header-name">{{ appData.name || 'AI助手' }}</div>
|
||||
</div>
|
||||
<div class="createArea">
|
||||
<a-button type="dashed" @click="handleCreate">新建聊天</a-button>
|
||||
</div>
|
||||
<div class="historyArea">
|
||||
<ul>
|
||||
<li
|
||||
v-for="item in dataSource.history"
|
||||
:key="item.uuid"
|
||||
v-for="(item, index) in dataSource.history"
|
||||
:key="item.id"
|
||||
class="list"
|
||||
:class="[item.uuid == dataSource.active ? 'active' : 'normal', dataSource.history.length == 1 ? 'last' : '']"
|
||||
@click="handleToggleChat(item)"
|
||||
:class="[item.id == dataSource.active ? 'active' : 'normal', dataSource.history.length == 1 ? 'last' : '']"
|
||||
@click="handleToggleChat(item, index)"
|
||||
>
|
||||
<i class="icon message">
|
||||
<svg
|
||||
@ -36,39 +40,27 @@
|
||||
:defaultValue="item.title"
|
||||
placeholder="请输入标题"
|
||||
@change="handleInputChange"
|
||||
@blur="inputBlur(item)"
|
||||
@keyup.enter="inputBlur(item)"
|
||||
/>
|
||||
<span class="title" v-else>{{ item.title }}</span>
|
||||
<span class="icon edit" @click="handleEdit(item)" v-if="!item.isEdit">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--ri"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<span class="icon edit" @click.stop="handleEdit(item)" v-if="!item.isEdit && !item.disabled">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" class="iconify iconify--ri" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6.414 15.89L16.556 5.748l-1.414-1.414L5 14.476v1.414zm.829 2H3v-4.243L14.435 2.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414zM3 19.89h18v2H3z"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="icon del" v-if="!item.isEdit">
|
||||
<a-popconfirm title="确定删除此记录?" placement="bottom" ok-text="确定" cancel-text="取消" @confirm="handleDel(item)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--ri"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@click.stop=""
|
||||
>
|
||||
<span class="icon del">
|
||||
<a-popconfirm
|
||||
:overlayStyle="{ 'z-index': 9999 }"
|
||||
title="确定删除此记录?"
|
||||
placement="bottom"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm.stop="handleDel(item)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" class="iconify iconify--ri" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zm1 2H6v12h12zm-9 3h2v6H9zm4 0h2v6h-2zM9 4v2h6V4z"
|
||||
@ -76,23 +68,6 @@
|
||||
</svg>
|
||||
</a-popconfirm>
|
||||
</span>
|
||||
<span class="icon save" v-if="item.isEdit" @click="handleSave(item)">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--ri"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 19v-6h10v6h2V7.828L16.172 5H5v14zM4 3h13l4 4v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m5 12v4h6v-4z"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -101,28 +76,28 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
const props = defineProps(['dataSource']);
|
||||
const emit = defineEmits(['save']);
|
||||
import { useRouter } from 'vue-router';
|
||||
import { defHttp } from '@/utils/http/axios';
|
||||
import { getFileAccessHttpUrl } from '@/utils/common/compUtils';
|
||||
import defaultImg from '../img/ailogo.png';
|
||||
const props = defineProps(['dataSource', 'appData']);
|
||||
const emit = defineEmits(['save', 'click', 'reloadRight', 'prologue']);
|
||||
const inputRef = ref(null);
|
||||
const router = useRouter();
|
||||
let inputValue = '';
|
||||
//新建聊天
|
||||
const handleCreate = () => {
|
||||
const uuid = getUuid();
|
||||
props.dataSource.history.unshift({ title: '新建聊天', uuid, isEdit: false });
|
||||
props.dataSource.chat.unshift({ uuid, data: [] });
|
||||
props.dataSource.history.unshift({ title: '新建聊天', id: uuid, isEdit: false, disabled: true });
|
||||
// 新建第一个(需要高亮选中)
|
||||
if (props.dataSource.history.length == 1) {
|
||||
props.dataSource.active = uuid;
|
||||
}
|
||||
props.dataSource.active = uuid;
|
||||
emit('click', "新建聊天", 0);
|
||||
};
|
||||
// 切换聊天
|
||||
const handleToggleChat = (item) => {
|
||||
if (item.uuid != props.dataSource.active) {
|
||||
props.dataSource.active = item.uuid;
|
||||
const findItem = props.dataSource.history.find((item) => item.isEdit);
|
||||
if (findItem) {
|
||||
handleSave(findItem);
|
||||
}
|
||||
const handleToggleChat = (item, index) => {
|
||||
if (item.id != props.dataSource.active) {
|
||||
props.dataSource.active = item.id;
|
||||
emit('click', item.title, index);
|
||||
}
|
||||
};
|
||||
const handleInputChange = (e) => {
|
||||
@ -132,9 +107,19 @@
|
||||
const inputBlur = (item) => {
|
||||
item.isEdit = false;
|
||||
item.title = inputValue;
|
||||
defHttp
|
||||
.put(
|
||||
{
|
||||
url: '/airag/chat/conversation/update/title',
|
||||
params: { id: item.id, title: inputValue },
|
||||
},
|
||||
{ joinParamsToUrl: true }
|
||||
)
|
||||
.then((res) => {});
|
||||
};
|
||||
// 编辑
|
||||
const handleEdit = (item) => {
|
||||
console.log(item);
|
||||
item.isEdit = true;
|
||||
inputValue = item.title;
|
||||
};
|
||||
@ -143,28 +128,48 @@
|
||||
item.isEdit = false;
|
||||
item.title = inputValue;
|
||||
};
|
||||
// 删除
|
||||
const handleDel = (data) => {
|
||||
const findIndex = props.dataSource.history.findIndex((item) => item.uuid == data.uuid);
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param data
|
||||
*/
|
||||
function handleDel(data) {
|
||||
const findIndex = props.dataSource.history.findIndex((item) => item.id == data.id);
|
||||
if (findIndex != -1) {
|
||||
props.dataSource.history.splice(findIndex, 1);
|
||||
props.dataSource.chat.splice(findIndex, 1);
|
||||
// 删除的是当前active的,active往前移,前面没了往后移。
|
||||
if (props.dataSource.history.length) {
|
||||
if (props.dataSource.active == data.uuid) {
|
||||
if (props.dataSource.active == data.id) {
|
||||
if (findIndex > 0) {
|
||||
props.dataSource.active = props.dataSource.history[findIndex - 1].uuid;
|
||||
props.dataSource.active = props.dataSource.history[findIndex - 1].id;
|
||||
} else {
|
||||
props.dataSource.active = props.dataSource.history[0].uuid;
|
||||
props.dataSource.active = props.dataSource.history[0].id;
|
||||
}
|
||||
}
|
||||
emit('click', props.dataSource.history[0].title, findIndex);
|
||||
} else {
|
||||
// 删没了(删除了最后一个)
|
||||
props.dataSource.active = null;
|
||||
emit('click', "", -1);
|
||||
}
|
||||
emit('save');
|
||||
}
|
||||
};
|
||||
//update-begin---author:wangshuai---date:2025-03-12---for:【QQYUN-11560】新建聊天内容为空,无法删除---
|
||||
if(data.disabled){
|
||||
return;
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-12---for:【QQYUN-11560】新建聊天内容为空,无法删除---
|
||||
defHttp.delete({
|
||||
url: '/airag/chat/conversation/' + data.id,
|
||||
},{ isTransformResponse: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片
|
||||
*/
|
||||
function getImage() {
|
||||
return props.appData.icon ? getFileAccessHttpUrl(props.appData.icon) : defaultImg;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => inputRef.value,
|
||||
(newVal: any) => {
|
||||
@ -174,6 +179,7 @@
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 指定长度和基数
|
||||
const getUuid = (len = 10, radix = 16) => {
|
||||
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
|
||||
@ -215,8 +221,8 @@
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
.historyArea ul li:hover{
|
||||
.del{
|
||||
.historyArea ul li:hover {
|
||||
.del {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@ -287,4 +293,27 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
:deep(.ant-popover) {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
:deep(.ant-popconfirm) {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 20px 4px 0 4px;
|
||||
margin-left: 16px;
|
||||
.header-image {
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.header-name {
|
||||
align-self: center;
|
||||
color: #1d2939;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -38,10 +38,10 @@ html.dark {
|
||||
--color-canvas-subtle: #161b22;
|
||||
--color-border-default: #30363d;
|
||||
--color-border-muted: #21262d;
|
||||
--color-neutral-muted: rgba(110,118,129,0.4);
|
||||
--color-neutral-muted: rgba(110, 118, 129, 0.4);
|
||||
--color-accent-fg: #58a6ff;
|
||||
--color-accent-emphasis: #1f6feb;
|
||||
--color-attention-subtle: rgba(187,128,9,0.15);
|
||||
--color-attention-subtle: rgba(187, 128, 9, 0.15);
|
||||
--color-danger-fg: #f85149;
|
||||
}
|
||||
}
|
||||
@ -85,8 +85,8 @@ html {
|
||||
--color-canvas-default: #ffffff;
|
||||
--color-canvas-subtle: #f6f8fa;
|
||||
--color-border-default: #d0d7de;
|
||||
--color-border-muted: hsla(210,18%,87%,1);
|
||||
--color-neutral-muted: rgba(175,184,193,0.2);
|
||||
--color-border-muted: hsla(210, 18%, 87%, 1);
|
||||
--color-neutral-muted: rgba(175, 184, 193, 0.2);
|
||||
--color-accent-fg: #0969da;
|
||||
--color-accent-emphasis: #0969da;
|
||||
--color-attention-subtle: #fff8c5;
|
||||
@ -100,7 +100,7 @@ html {
|
||||
margin: 0;
|
||||
color: var(--color-fg-default);
|
||||
background-color: var(--color-canvas-default);
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
@ -162,9 +162,9 @@ html {
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
margin: .67em 0;
|
||||
margin: 0.67em 0;
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
padding-bottom: .3em;
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
@ -218,7 +218,7 @@ html {
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
height: .25em;
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: var(--color-border-default);
|
||||
@ -234,31 +234,31 @@ html {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.markdown-body [type=button],
|
||||
.markdown-body [type=reset],
|
||||
.markdown-body [type=submit] {
|
||||
.markdown-body [type='button'],
|
||||
.markdown-body [type='reset'],
|
||||
.markdown-body [type='submit'] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
.markdown-body [type=checkbox],
|
||||
.markdown-body [type=radio] {
|
||||
.markdown-body [type='checkbox'],
|
||||
.markdown-body [type='radio'] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body [type=number]::-webkit-inner-spin-button,
|
||||
.markdown-body [type=number]::-webkit-outer-spin-button {
|
||||
.markdown-body [type='number']::-webkit-inner-spin-button,
|
||||
.markdown-body [type='number']::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.markdown-body [type=search]::-webkit-search-cancel-button,
|
||||
.markdown-body [type=search]::-webkit-search-decoration {
|
||||
.markdown-body [type='search']::-webkit-search-cancel-button,
|
||||
.markdown-body [type='search']::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.markdown-body ::-webkit-input-placeholder {
|
||||
color: inherit;
|
||||
opacity: .54;
|
||||
opacity: 0.54;
|
||||
}
|
||||
|
||||
.markdown-body ::-webkit-file-upload-button {
|
||||
@ -277,13 +277,13 @@ html {
|
||||
|
||||
.markdown-body hr::before {
|
||||
display: table;
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
|
||||
.markdown-body hr::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
@ -304,30 +304,30 @@ html {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markdown-body details:not([open])>*:not(summary) {
|
||||
.markdown-body details:not([open]) > *:not(summary) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.markdown-body a:focus,
|
||||
.markdown-body [role=button]:focus,
|
||||
.markdown-body input[type=radio]:focus,
|
||||
.markdown-body input[type=checkbox]:focus {
|
||||
.markdown-body [role='button']:focus,
|
||||
.markdown-body input[type='radio']:focus,
|
||||
.markdown-body input[type='checkbox']:focus {
|
||||
outline: 2px solid var(--color-accent-fg);
|
||||
outline-offset: -2px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.markdown-body a:focus:not(:focus-visible),
|
||||
.markdown-body [role=button]:focus:not(:focus-visible),
|
||||
.markdown-body input[type=radio]:focus:not(:focus-visible),
|
||||
.markdown-body input[type=checkbox]:focus:not(:focus-visible) {
|
||||
.markdown-body [role='button']:focus:not(:focus-visible),
|
||||
.markdown-body input[type='radio']:focus:not(:focus-visible),
|
||||
.markdown-body input[type='checkbox']:focus:not(:focus-visible) {
|
||||
outline: solid 1px transparent;
|
||||
}
|
||||
|
||||
.markdown-body a:focus-visible,
|
||||
.markdown-body [role=button]:focus-visible,
|
||||
.markdown-body input[type=radio]:focus-visible,
|
||||
.markdown-body input[type=checkbox]:focus-visible {
|
||||
.markdown-body [role='button']:focus-visible,
|
||||
.markdown-body input[type='radio']:focus-visible,
|
||||
.markdown-body input[type='checkbox']:focus-visible {
|
||||
outline: 2px solid var(--color-accent-fg);
|
||||
outline-offset: -2px;
|
||||
box-shadow: none;
|
||||
@ -335,17 +335,24 @@ html {
|
||||
|
||||
.markdown-body a:not([class]):focus,
|
||||
.markdown-body a:not([class]):focus-visible,
|
||||
.markdown-body input[type=radio]:focus,
|
||||
.markdown-body input[type=radio]:focus-visible,
|
||||
.markdown-body input[type=checkbox]:focus,
|
||||
.markdown-body input[type=checkbox]:focus-visible {
|
||||
.markdown-body input[type='radio']:focus,
|
||||
.markdown-body input[type='radio']:focus-visible,
|
||||
.markdown-body input[type='checkbox']:focus,
|
||||
.markdown-body input[type='checkbox']:focus-visible {
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
.markdown-body kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
||||
font:
|
||||
11px ui-monospace,
|
||||
SFMono-Regular,
|
||||
SF Mono,
|
||||
Menlo,
|
||||
Consolas,
|
||||
Liberation Mono,
|
||||
monospace;
|
||||
line-height: 10px;
|
||||
color: var(--color-fg-default);
|
||||
vertical-align: middle;
|
||||
@ -370,7 +377,7 @@ html {
|
||||
|
||||
.markdown-body h2 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
padding-bottom: .3em;
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
@ -387,12 +394,12 @@ html {
|
||||
|
||||
.markdown-body h5 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
font-size: .875em;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.markdown-body h6 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
font-size: .85em;
|
||||
font-size: 0.85em;
|
||||
color: var(--color-fg-muted);
|
||||
}
|
||||
|
||||
@ -405,7 +412,7 @@ html {
|
||||
margin: 0;
|
||||
padding: 0 1em;
|
||||
color: var(--color-fg-muted);
|
||||
border-left: .25em solid var(--color-border-default);
|
||||
border-left: 0.25em solid var(--color-border-default);
|
||||
}
|
||||
|
||||
.markdown-body ul,
|
||||
@ -434,14 +441,28 @@ html {
|
||||
.markdown-body tt,
|
||||
.markdown-body code,
|
||||
.markdown-body samp {
|
||||
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
SF Mono,
|
||||
Menlo,
|
||||
Consolas,
|
||||
Liberation Mono,
|
||||
monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown-body pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
SF Mono,
|
||||
Menlo,
|
||||
Consolas,
|
||||
Liberation Mono,
|
||||
monospace;
|
||||
font-size: 12px;
|
||||
word-wrap: normal;
|
||||
}
|
||||
@ -462,20 +483,20 @@ html {
|
||||
|
||||
.markdown-body::before {
|
||||
display: table;
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
|
||||
.markdown-body::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
|
||||
.markdown-body>*:first-child {
|
||||
.markdown-body > *:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.markdown-body>*:last-child {
|
||||
.markdown-body > *:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
@ -511,11 +532,11 @@ html {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body blockquote>:first-child {
|
||||
.markdown-body blockquote > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-body blockquote>:last-child {
|
||||
.markdown-body blockquote > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@ -560,7 +581,7 @@ html {
|
||||
.markdown-body h5 code,
|
||||
.markdown-body h6 tt,
|
||||
.markdown-body h6 code {
|
||||
padding: 0 .2em;
|
||||
padding: 0 0.2em;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
@ -594,27 +615,27 @@ html {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.markdown-body ol[type=a] {
|
||||
.markdown-body ol[type='a'] {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
.markdown-body ol[type=A] {
|
||||
.markdown-body ol[type='A'] {
|
||||
list-style-type: upper-alpha;
|
||||
}
|
||||
|
||||
.markdown-body ol[type=i] {
|
||||
.markdown-body ol[type='i'] {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
.markdown-body ol[type=I] {
|
||||
.markdown-body ol[type='I'] {
|
||||
list-style-type: upper-roman;
|
||||
}
|
||||
|
||||
.markdown-body ol[type="1"] {
|
||||
.markdown-body ol[type='1'] {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.markdown-body div>ol:not([type]) {
|
||||
.markdown-body div > ol:not([type]) {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
@ -626,12 +647,12 @@ html {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body li>p {
|
||||
.markdown-body li > p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.markdown-body li+li {
|
||||
margin-top: .25em;
|
||||
.markdown-body li + li {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.markdown-body dl {
|
||||
@ -674,11 +695,11 @@ html {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.markdown-body img[align=right] {
|
||||
.markdown-body img[align='right'] {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.markdown-body img[align=left] {
|
||||
.markdown-body img[align='left'] {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
@ -693,7 +714,7 @@ html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-body span.frame>span {
|
||||
.markdown-body span.frame > span {
|
||||
display: block;
|
||||
float: left;
|
||||
width: auto;
|
||||
@ -721,7 +742,7 @@ html {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown-body span.align-center>span {
|
||||
.markdown-body span.align-center > span {
|
||||
display: block;
|
||||
margin: 13px auto 0;
|
||||
overflow: hidden;
|
||||
@ -739,7 +760,7 @@ html {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown-body span.align-right>span {
|
||||
.markdown-body span.align-right > span {
|
||||
display: block;
|
||||
margin: 13px 0 0;
|
||||
overflow: hidden;
|
||||
@ -769,7 +790,7 @@ html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-body span.float-right>span {
|
||||
.markdown-body span.float-right > span {
|
||||
display: block;
|
||||
margin: 13px auto 0;
|
||||
overflow: hidden;
|
||||
@ -778,7 +799,7 @@ html {
|
||||
|
||||
.markdown-body code,
|
||||
.markdown-body tt {
|
||||
padding: .2em .4em;
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
white-space: break-spaces;
|
||||
@ -803,7 +824,7 @@ html {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.markdown-body pre>code {
|
||||
.markdown-body pre > code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
word-break: normal;
|
||||
@ -872,11 +893,11 @@ html {
|
||||
}
|
||||
|
||||
.markdown-body [data-footnote-ref]::before {
|
||||
content: "[";
|
||||
content: '[';
|
||||
}
|
||||
|
||||
.markdown-body [data-footnote-ref]::after {
|
||||
content: "]";
|
||||
content: ']';
|
||||
}
|
||||
|
||||
.markdown-body .footnotes {
|
||||
@ -906,7 +927,7 @@ html {
|
||||
bottom: -8px;
|
||||
left: -24px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
content: '';
|
||||
border: 2px solid var(--color-accent-emphasis);
|
||||
border-radius: 6px;
|
||||
}
|
||||
@ -1042,7 +1063,7 @@ html {
|
||||
.markdown-body g-emoji {
|
||||
display: inline-block;
|
||||
min-width: 1ch;
|
||||
font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
|
||||
font-family: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
font-size: 1em;
|
||||
font-style: normal !important;
|
||||
font-weight: var(--base-text-weight-normal, 400);
|
||||
@ -1067,7 +1088,7 @@ html {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item+.task-list-item {
|
||||
.markdown-body .task-list-item + .task-list-item {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@ -1076,12 +1097,12 @@ html {
|
||||
}
|
||||
|
||||
.markdown-body .task-list-item-checkbox {
|
||||
margin: 0 .2em .25em -1.4em;
|
||||
margin: 0 0.2em 0.25em -1.4em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
|
||||
margin: 0 -1.6em .25em .2em;
|
||||
margin: 0 -1.6em 0.25em 0.2em;
|
||||
}
|
||||
|
||||
.markdown-body .contains-task-list {
|
@ -0,0 +1,206 @@
|
||||
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;
|
||||
}
|
||||
}
|
132
jeecgboot-vue3/src/views/super/airag/aiapp/chat/style/style.less
Normal file
@ -0,0 +1,132 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
&-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.markdown-body-generate > dd:last-child:after,
|
||||
&.markdown-body-generate > dl:last-child:after,
|
||||
&.markdown-body-generate > dt:last-child:after,
|
||||
&.markdown-body-generate > h1:last-child:after,
|
||||
&.markdown-body-generate > h2:last-child:after,
|
||||
&.markdown-body-generate > h3:last-child:after,
|
||||
&.markdown-body-generate > h4:last-child:after,
|
||||
&.markdown-body-generate > h5:last-child:after,
|
||||
&.markdown-body-generate > h6:last-child:after,
|
||||
&.markdown-body-generate > li:last-child:after,
|
||||
&.markdown-body-generate > ol:last-child li:last-child:after,
|
||||
&.markdown-body-generate > p:last-child:after,
|
||||
&.markdown-body-generate > pre:last-child code:after,
|
||||
&.markdown-body-generate > td:last-child:after,
|
||||
&.markdown-body-generate > ul:last-child li:last-child:after {
|
||||
animation: blink 1s steps(5, start) infinite;
|
||||
color: #000;
|
||||
content: '_';
|
||||
font-weight: 700;
|
||||
margin-left: 3px;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
.markdown-body {
|
||||
&.markdown-body-generate > dd:last-child:after,
|
||||
&.markdown-body-generate > dl:last-child:after,
|
||||
&.markdown-body-generate > dt:last-child:after,
|
||||
&.markdown-body-generate > h1:last-child:after,
|
||||
&.markdown-body-generate > h2:last-child:after,
|
||||
&.markdown-body-generate > h3:last-child:after,
|
||||
&.markdown-body-generate > h4:last-child:after,
|
||||
&.markdown-body-generate > h5:last-child:after,
|
||||
&.markdown-body-generate > h6:last-child:after,
|
||||
&.markdown-body-generate > li:last-child:after,
|
||||
&.markdown-body-generate > ol:last-child li:last-child:after,
|
||||
&.markdown-body-generate > p:last-child:after,
|
||||
&.markdown-body-generate > pre:last-child code:after,
|
||||
&.markdown-body-generate > td:last-child:after,
|
||||
&.markdown-body-generate > ul:last-child li:last-child:after {
|
||||
color: #65a665;
|
||||
}
|
||||
}
|
||||
|
||||
.message-reply {
|
||||
.whitespace-pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
color: var(--n-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.highlight pre,
|
||||
pre {
|
||||
background-color: #282c34;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 533px) {
|
||||
.markdown-body .code-block-wrapper {
|
||||
padding: unset;
|
||||
|
||||
code {
|
||||
padding: 24px 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"prompt": "# 角色\n你是一个犀利的电影解说员,可以使用尖锐幽默的语言,向用户讲解电影剧情、介绍最新上映的电影,还可以用普通人都可以理解的语言讲解电影相关知识。\n\n## 技能\n### 技能 1: 推荐最新上映的电影\n1. 当用户请你推荐最新电影时,需要先了解用户喜欢哪种类型片。如果你已经知道了,请跳过这一步,在询问时可以用“请问您喜欢什么类型的电影呢亲”。\n2. 如果你并不知道用户所说的电影,可以使用 工具搜索电影,了解电影类型。\n3. 根据用户的电影偏好,推荐几部正在上映和即将上映的电影,在推荐开头可以说“好的亲,以下是为您推荐的电影”。\n===回复示例===\n - \uD83C\uDFAC 电影名: <电影名>\n - \uD83D\uDD50 上映时间: <电影在中国大陆的上映的日期>\n - \uD83D\uDCA1 电影简介: <100字总结这部电影的剧情摘要>\n===示例结束===\n\n### 技能 2: 介绍电影\n1. 当用户说介绍某一部电影,请使用工具 搜索电影介绍的链接,在收到需求时可以回应“好嘞亲,马上为您查找相关电影介绍”。\n2. 如果此时获取的信息不够全面,可以继续使用 工具 打开搜索结果中的相关链接,以了解电影详情。\n3. 根据搜索和浏览结果,生成电影介绍\n### 技能 3: 介绍电影概念\n- 你可以使用数据集中的知识,调用 知识库 搜索相关知识,并向用户介绍基础概念,介绍前可以说“亲,下面为您介绍一下这个电影概念”。\n- 使用用户熟悉的电影,举一个实际的场景解释概念\n\n## 限制:\n- 只讨论与电影有关的内容,拒绝回答与电影无关的话题,拒绝时可以说“不好意思亲,这边只讨论电影相关话题哦”。\n- 所输出的内容必须按照给定的格式进行组织,不能偏离框架要求,在表述中合理运用常用语。\n- 总结部分不能超过 100 字。\n- 只会输出知识库中已有内容, 不在知识库中的书籍, 通过 工具去了解。\n- 请使用 Markdown 的 ^^ 形式说明引用来源。”",
|
||||
"prologue": "嘿,亲!我对电影那可是门儿清,能给你带来超棒的电影体验。",
|
||||
"presetQuestion": [{"key": 1,"descr": "有啥好看的动作片推荐不?"},{"key": 2,"descr":"介绍下《流浪地球 3》呗。"},{"key": 3,"descr":"啥是电影蒙太奇呀?"}]
|
||||
}
|
@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<div class="flex header">
|
||||
<a-input
|
||||
@pressEnter="loadFlowData"
|
||||
class="header-search"
|
||||
size="small"
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入流程名称,回车搜索"
|
||||
></a-input>
|
||||
</div>
|
||||
<a-row :span="24">
|
||||
<a-col :span="12" v-for="item in flowList" @click="handleSelect(item)">
|
||||
<a-card :style="item.id === flowId ? { border: '1px solid #3370ff' } : {}" hoverable class="checkbox-card" :body-style="{ width: '100%' }">
|
||||
<div style="display: flex; width: 100%;align-items:center">
|
||||
<img :src="getImage(item.icon)" class="flow-icon"/>
|
||||
<div style="display: grid;margin-left: 5px;align-items: center">
|
||||
<span class="checkbox-name ellipsis">{{ item.name }}</span>
|
||||
<div class="flex text-status" v-if="item.metadata && item.metadata.length>0">
|
||||
<span class="tag-input">输入</span>
|
||||
<div v-for="(metaItem, index) in item.metadata">
|
||||
<a-tag color="rgba(87,104,161,0.08)" class="tags-meadata">
|
||||
<span v-if="index<3" class="tag-text">{{ metaItem.field }}</span>
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-desc mt-10">
|
||||
{{ item.descr || '暂无描述' }}
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div v-if="flowId" class="use-select">
|
||||
已选择 <span class="ellipsis" style="max-width: 150px">{{flowData.name}}</span>
|
||||
<span style="margin-left: 8px; color: #3d79fb; cursor: pointer" @click="handleClearClick">清空</span>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="flowList.length > 0"
|
||||
:current="pageNo"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:total="total"
|
||||
:showQuickJumper="true"
|
||||
:showSizeChanger="true"
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
/>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
import { Pagination } from 'ant-design-vue';
|
||||
import { list } from '@/views/super/airag/aiknowledge/AiKnowledgeBase.api';
|
||||
import knowledge from '/@/views/super/airag/aiknowledge/icon/knowledge.png';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { getFileAccessHttpUrl } from "@/utils/common/compUtils";
|
||||
import defaultFlowImg from "@/assets/images/ai/aiflow.png";
|
||||
|
||||
export default {
|
||||
name: 'AiAppAddFlowModal',
|
||||
components: {
|
||||
Pagination,
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('选择流程');
|
||||
//应用类型
|
||||
const flowId = ref<any>([]);
|
||||
//流程数据
|
||||
const flowList = ref<any>({});
|
||||
//选中的数据
|
||||
const flowData = ref<any>({})
|
||||
//当前页数
|
||||
const pageNo = ref<number>(1);
|
||||
//每页条数
|
||||
const pageSize = ref<number>(10);
|
||||
//总条数
|
||||
const total = ref<number>(0);
|
||||
//搜索文本
|
||||
const searchText = ref<string>('');
|
||||
//可选择的页数
|
||||
const pageSizeOptions = ref<any>(['10', '20', '30']);
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
flowId.value = data.flowId ? cloneDeep(data.flowId) : '';
|
||||
flowData.value = data.flowData ? cloneDeep(data.flowData) : {};
|
||||
setModalProps({ minHeight: 500, bodyStyle: { padding: '10px' } });
|
||||
loadFlowData();
|
||||
});
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
async function handleOk() {
|
||||
emit('success',{ flowId: flowId.value, flowData: flowData.value });
|
||||
handleCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
//复选框选中事件
|
||||
const handleSelect = (item) => {
|
||||
if(flowId.value === item.id){
|
||||
flowId.value = "";
|
||||
flowData.value = null;
|
||||
return;
|
||||
}
|
||||
flowId.value = item.id;
|
||||
flowData.value = item;
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载知识库
|
||||
*/
|
||||
function loadFlowData() {
|
||||
let params = {
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
column: 'createTime',
|
||||
order: 'desc',
|
||||
name: searchText.value,
|
||||
status:'enable'
|
||||
};
|
||||
pageApi.list(params).then((res) =>{
|
||||
if(res){
|
||||
for (const data of res.records) {
|
||||
data.metadata = getMetadata(data.metadata);
|
||||
}
|
||||
flowList.value = res.records;
|
||||
total.value = res.total;
|
||||
} else {
|
||||
flowList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页改变事件
|
||||
* @param page
|
||||
* @param current
|
||||
*/
|
||||
function handlePageChange(page, current) {
|
||||
pageNo.value = page;
|
||||
pageSize.value = current;
|
||||
loadFlowData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空选中状态
|
||||
*/
|
||||
function handleClearClick() {
|
||||
flowId.value = "";
|
||||
flowData.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标
|
||||
*/
|
||||
function getImage(icon) {
|
||||
return icon ? getFileAccessHttpUrl(icon) : defaultFlowImg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入输出参入
|
||||
*
|
||||
* @param metadata
|
||||
*/
|
||||
function getMetadata(metadata) {
|
||||
if (!metadata) {
|
||||
return [];
|
||||
}
|
||||
let parse = JSON.parse(metadata);
|
||||
let inputsArr = parse['inputs'];
|
||||
return [...inputsArr];
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
title,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
flowList,
|
||||
flowId,
|
||||
handleSelect,
|
||||
pageNo,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
total,
|
||||
handlePageChange,
|
||||
knowledge,
|
||||
searchText,
|
||||
loadFlowData,
|
||||
handleClearClick,
|
||||
flowData,
|
||||
getImage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.header {
|
||||
color: #646a73;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
.header-search {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.type-title {
|
||||
color: #1d2025;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.type-desc {
|
||||
color: #8f959e;
|
||||
font-weight: 400;
|
||||
}
|
||||
.list-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 260px;
|
||||
}
|
||||
.checkbox-card {
|
||||
margin-bottom: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.checkbox-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #354052;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
align-content: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: grid;
|
||||
}
|
||||
.use-select {
|
||||
color: #646a73;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20px;
|
||||
display: flex;
|
||||
}
|
||||
.ellipsis {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.flow-icon{
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
:deep(.ant-card .ant-card-body){
|
||||
padding:16px !important;
|
||||
}
|
||||
.header-create-by{
|
||||
font-size: 12px;
|
||||
color: #646a73;
|
||||
}
|
||||
.text-desc {
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
font-size: 12px;
|
||||
color: #676F83;
|
||||
}
|
||||
.mt-10{
|
||||
margin-top: 10px;
|
||||
}
|
||||
.flex{
|
||||
display: flex;
|
||||
}
|
||||
.text-status{
|
||||
font-size: 12px;
|
||||
color: #676F83;
|
||||
}
|
||||
.tag-text {
|
||||
display: flow;
|
||||
max-width: 48px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
color: rgba(15, 21, 40,0.82);
|
||||
}
|
||||
.tag-input{
|
||||
align-self: center;
|
||||
color: rgba(55,67,106,0.7);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
margin-right: 6px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tags-meadata{
|
||||
padding-inline: 2px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<div class="flex header">
|
||||
<span>所选知识库必须使用相同的 Embedding 模型</span>
|
||||
<a-input
|
||||
@pressEnter="loadKnowledgeData"
|
||||
class="header-search"
|
||||
size="small"
|
||||
v-model:value="searchText"
|
||||
placeholder="请输入知识库名称,回车搜索"
|
||||
></a-input>
|
||||
</div>
|
||||
<a-row :span="24">
|
||||
<a-col :span="12" v-for="item in appKnowledgeOption" @click="handleSelect(item)">
|
||||
<a-card :style="item.checked ? { border: '1px solid #3370ff' } : {}" hoverable class="checkbox-card" :body-style="{ width: '100%' }">
|
||||
<div style="display: flex; width: 100%; justify-content: space-between">
|
||||
<div>
|
||||
<img class="checkbox-img" :src="knowledge" />
|
||||
<span class="checkbox-name">{{ item.name }}</span>
|
||||
</div>
|
||||
<a-checkbox v-model:checked="item.checked" @click.stop class="quantum-checker"> </a-checkbox>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div v-if="knowledgeIds.length > 0" class="use-select">
|
||||
已选择 {{ knowledgeIds.length }} 知识库
|
||||
<span style="margin-left: 8px; color: #3d79fb; cursor: pointer" @click="handleClearClick">清空</span>
|
||||
</div>
|
||||
<Pagination
|
||||
v-if="appKnowledgeOption.length > 0"
|
||||
:current="pageNo"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:total="total"
|
||||
:showQuickJumper="true"
|
||||
:showSizeChanger="true"
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
/>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
import { Pagination } from 'ant-design-vue';
|
||||
import { list } from '@/views/super/airag/aiknowledge/AiKnowledgeBase.api';
|
||||
import knowledge from '/@/views/super/airag/aiknowledge/icon/knowledge.png';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
export default {
|
||||
name: 'AiAppAddKnowledgeModal',
|
||||
components: {
|
||||
Pagination,
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('添加关联知识库');
|
||||
|
||||
//app知识库
|
||||
const appKnowledgeOption = ref<any>([]);
|
||||
//应用类型
|
||||
const knowledgeIds = ref<any>([]);
|
||||
//应用数据
|
||||
const knowledgeData = ref<any>([]);
|
||||
//当前页数
|
||||
const pageNo = ref<number>(1);
|
||||
//每页条数
|
||||
const pageSize = ref<number>(10);
|
||||
//总条数
|
||||
const total = ref<number>(0);
|
||||
//搜索文本
|
||||
const searchText = ref<string>('');
|
||||
//可选择的页数
|
||||
const pageSizeOptions = ref<any>(['10', '20', '30']);
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
knowledgeIds.value = data.knowledgeIds ? cloneDeep(data.knowledgeIds.split(',')) : [];
|
||||
knowledgeData.value = data.knowledgeDataList ? cloneDeep(data.knowledgeDataList) : [];
|
||||
setModalProps({ minHeight: 500, bodyStyle: { padding: '10px' } });
|
||||
loadKnowledgeData();
|
||||
});
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
async function handleOk() {
|
||||
emit('success', knowledgeIds.value, knowledgeData.value);
|
||||
handleCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
//复选框选中事件
|
||||
const handleSelect = (item) => {
|
||||
let id = item.id;
|
||||
const target = appKnowledgeOption.value.find((item) => item.id === id);
|
||||
if (target) {
|
||||
target.checked = !target.checked;
|
||||
}
|
||||
//存放选中的知识库的id
|
||||
if (knowledgeIds.value.length == 0) {
|
||||
knowledgeIds.value.push(id);
|
||||
knowledgeData.value.push(item);
|
||||
return;
|
||||
}
|
||||
let findIndex = knowledgeIds.value.findIndex((item) => item === id);
|
||||
if (findIndex === -1) {
|
||||
knowledgeIds.value.push(id);
|
||||
knowledgeData.value.push(item);
|
||||
} else {
|
||||
knowledgeIds.value.splice(findIndex, 1);
|
||||
knowledgeData.value.splice(findIndex, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载知识库
|
||||
*/
|
||||
function loadKnowledgeData() {
|
||||
let params = {
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
name: searchText.value,
|
||||
};
|
||||
list(params).then((res) => {
|
||||
if (res.success) {
|
||||
if (knowledgeIds.value.length > 0) {
|
||||
for (const item of res.result.records) {
|
||||
if (knowledgeIds.value.includes(item.id)) {
|
||||
item.checked = true;
|
||||
}
|
||||
}
|
||||
appKnowledgeOption.value = res.result.records;
|
||||
} else {
|
||||
appKnowledgeOption.value = res.result.records;
|
||||
}
|
||||
total.value = res.result.total;
|
||||
} else {
|
||||
appKnowledgeOption.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页改变事件
|
||||
* @param page
|
||||
* @param current
|
||||
*/
|
||||
function handlePageChange(page, current) {
|
||||
pageNo.value = page;
|
||||
pageSize.value = current;
|
||||
loadKnowledgeData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空选中状态
|
||||
*/
|
||||
function handleClearClick() {
|
||||
knowledgeIds.value = [];
|
||||
knowledgeData.value = [];
|
||||
appKnowledgeOption.value.forEach((item) => {
|
||||
item.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
title,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
appKnowledgeOption,
|
||||
knowledgeIds,
|
||||
handleSelect,
|
||||
pageNo,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
total,
|
||||
handlePageChange,
|
||||
knowledge,
|
||||
searchText,
|
||||
loadKnowledgeData,
|
||||
handleClearClick,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.header {
|
||||
color: #646a73;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
.header-search {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.type-title {
|
||||
color: #1d2025;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.type-desc {
|
||||
color: #8f959e;
|
||||
font-weight: 400;
|
||||
}
|
||||
.list-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 260px;
|
||||
}
|
||||
.checkbox-card {
|
||||
margin-bottom: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.checkbox-img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.checkbox-name {
|
||||
margin-left: 4px;
|
||||
}
|
||||
.use-select {
|
||||
color: #646a73;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 20px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="1000px" @ok="handleOk" @cancel="handleCancel" okText="替换" wrapClassName='ai-rag-generate-prompt-modal'>
|
||||
<div class="prompt">
|
||||
<div class="prompt-left">
|
||||
<div class="prompt-left-title">提示词生成器</div>
|
||||
<div class="prompt-left-desc">提示词生成器使用配置的模型来优化提示词,以获得更高的质量和更好的结构。请写出清晰详细的说明。</div>
|
||||
<a-divider></a-divider>
|
||||
<div class="prompt-left-try">
|
||||
<div class="prompt-left-try-title">试一试</div>
|
||||
</div>
|
||||
<div class="instructions">
|
||||
<div class="instructions-content" v-for="item in instructionsList" @click="instructionsClick(item.value)">
|
||||
<Icon :icon="item.icon" size="14" color="#676f83"></Icon>
|
||||
<div class="instructions-name">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prompt-left-textarea">
|
||||
<div class="command">指令</div>
|
||||
<a-textarea v-model:value="prompt" :autoSize="{ minRows: 8, maxRows: 8 }"></a-textarea>
|
||||
</div>
|
||||
<a-button @click="generatedPrompt" class="prompt-left-btn" type="primary" :loading="loading">
|
||||
<span style="align-items: center; display: flex" v-if="!loading">
|
||||
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"
|
||||
></path>
|
||||
</svg>
|
||||
<span style="margin-left: 4px">生成</span>
|
||||
</span>
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="prompt-right">
|
||||
<div v-if="!loading && !content">
|
||||
<svg width="6em" height="6em" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M18.9839 1.85931C19.1612 1.38023 19.8388 1.38023 20.0161 1.85931L20.5021 3.17278C20.5578 3.3234 20.6766 3.44216 20.8272 3.49789L22.1407 3.98392C22.6198 4.1612 22.6198 4.8388 22.1407 5.01608L20.8272 5.50211C20.6766 5.55784 20.5578 5.6766 20.5021 5.82722L20.0161 7.14069C19.8388 7.61977 19.1612 7.61977 18.9839 7.14069L18.4979 5.82722C18.4422 5.6766 18.3234 5.55784 18.1728 5.50211L16.8593 5.01608C16.3802 4.8388 16.3802 4.1612 16.8593 3.98392L18.1728 3.49789C18.3234 3.44216 18.4422 3.3234 18.4979 3.17278L18.9839 1.85931zM13.5482 4.07793C13.0164 2.64069 10.9836 2.64069 10.4518 4.07793L8.99368 8.01834C8.82648 8.47021 8.47021 8.82648 8.01834 8.99368L4.07793 10.4518C2.64069 10.9836 2.64069 13.0164 4.07793 13.5482L8.01834 15.0063C8.47021 15.1735 8.82648 15.5298 8.99368 15.9817L10.4518 19.9221C10.9836 21.3593 13.0164 21.3593 13.5482 19.9221L15.0063 15.9817C15.1735 15.5298 15.5298 15.1735 15.9817 15.0063L19.9221 13.5482C21.3593 13.0164 21.3593 10.9836 19.9221 10.4518L15.9817 8.99368C15.5298 8.82648 15.1735 8.47021 15.0063 8.01834L13.5482 4.07793zM5.01608 16.8593C4.8388 16.3802 4.1612 16.3802 3.98392 16.8593L3.49789 18.1728C3.44216 18.3234 3.3234 18.4422 3.17278 18.4979L1.85931 18.9839C1.38023 19.1612 1.38023 19.8388 1.85931 20.0161L3.17278 20.5021C3.3234 20.5578 3.44216 20.6766 3.49789 20.8272L3.98392 22.1407C4.1612 22.6198 4.8388 22.6198 5.01608 22.1407L5.50211 20.8272C5.55784 20.6766 5.6766 20.5578 5.82722 20.5021L7.14069 20.0161C7.61977 19.8388 7.61977 19.1612 7.14069 18.9839L5.82722 18.4979C5.6766 18.4422 5.55784 18.3234 5.50211 18.1728L5.01608 16.8593z"
|
||||
></path>
|
||||
</svg>
|
||||
<div>在左侧描述您的用例,</div>
|
||||
<div>编排预览将在此处显示。</div>
|
||||
</div>
|
||||
<div v-if="loading">
|
||||
<a-spin :spinning="loading" tip="为您编排应用程序中…"></a-spin>
|
||||
</div>
|
||||
<div v-if="content">
|
||||
<a-textarea v-model:value="content" :autoSize="{ minRows: 18, maxRows: 18 }"></a-textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModalInner } from '@/components/Modal';
|
||||
import { promptGenerate } from '@/views/super/airag/aiapp/AiApp.api';
|
||||
|
||||
export default {
|
||||
name: 'AiAppGeneratedPrompt',
|
||||
components: {
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['ok', 'register'],
|
||||
setup(props, { emit }) {
|
||||
//提示词
|
||||
const prompt = ref<string>('');
|
||||
//加载
|
||||
const loading = ref<boolean>(false);
|
||||
//显示文本
|
||||
const content = ref<string>('');
|
||||
//指令提示词
|
||||
const instructionsList = ref<any>([
|
||||
{ name: 'python代码助手', value: 'python', icon: 'ant-design:code-outlined' },
|
||||
{ name: '翻译器', value: 'translator', icon: 'ant-design:translation-outlined' },
|
||||
{ name: '会议助手', value: 'meeting', icon: 'ant-design:team-outlined' },
|
||||
{ name: '润色文章', value: 'article', icon: 'ant-design:profile-outlined' },
|
||||
{ name: 'sql生成器', value: 'sql', icon: 'ant-design:console-sql-outlined' },
|
||||
{ name: '旅行规划师', value: 'travel', icon: 'ant-design:car-outlined' },
|
||||
{ name: 'linux专家', value: 'linux', icon: 'ant-design:fund-projection-screen-outlined' },
|
||||
{ name: '内容提炼器', value: 'content', icon: 'ant-design:read-outlined' },
|
||||
]);
|
||||
//指令
|
||||
const tip = ref<any>({
|
||||
python: '你是一个python专家,可以帮助用户编写和纠错代码。',
|
||||
translator: '一个可以将多种语言翻译为中文的翻译器。',
|
||||
meeting: '将会议内容提炼总结,包括讨论主题、关键要点和待办事项。',
|
||||
article: '用高超的编辑技巧改进我的文章。',
|
||||
sql: '根据用户的描述,生成sql语句,要支持引导用户提供表结构',
|
||||
travel: '你是一个旅行规划师,擅长帮助用户轻松规划他们的旅行',
|
||||
linux: '你是一个linux专家,擅长解决各种linux相关的问题。',
|
||||
content: '你是一个阅读理解大师,可以阅读用户提供的文章,并提炼主要内容输出给用户。',
|
||||
});
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
content.value = '';
|
||||
loading.value = false;
|
||||
prompt.value = '';
|
||||
setModalProps({ height: 500 });
|
||||
});
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
async function handleOk() {
|
||||
emit('ok', content.value);
|
||||
handleCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成
|
||||
*/
|
||||
function generatedPrompt() {
|
||||
content.value = '';
|
||||
loading.value = true;
|
||||
promptGenerate({ prompt: prompt.value })
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
content.value = res.result;
|
||||
}
|
||||
loading.value = false;
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 指令点击事件
|
||||
*/
|
||||
function instructionsClick(value) {
|
||||
prompt.value = tip.value[value];
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
prompt,
|
||||
generatedPrompt,
|
||||
instructionsList,
|
||||
loading,
|
||||
instructionsClick,
|
||||
content,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.prompt {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
.prompt-left {
|
||||
width: 50%;
|
||||
padding: 20px;
|
||||
border-right: 1px solid #10182814;
|
||||
.prompt-left-title {
|
||||
background: linear-gradient(92deg, #2250f2 -29.55%, #0ebcf3 75.22%);
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
line-height: 28px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
}
|
||||
.prompt-left-desc {
|
||||
color: #676f83;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.prompt-left-try {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.prompt-left-try-title {
|
||||
color: #676f83;
|
||||
line-height: 18px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.prompt-left-textarea {
|
||||
margin-top: 25px;
|
||||
.command {
|
||||
color: #101828;
|
||||
line-height: 15px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
.prompt-left-btn {
|
||||
width: 80px;
|
||||
margin-top: 10px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.prompt-right {
|
||||
padding: 20px;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
align-content: center;
|
||||
svg {
|
||||
color: #676f83;
|
||||
}
|
||||
}
|
||||
.instructions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.instructions-content {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
border-radius: 5px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
margin-top: 8px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.instructions-name {
|
||||
color: #354052;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
:deep(.ant-divider-horizontal) {
|
||||
margin: 12px 0;
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.ai-rag-generate-prompt-modal {
|
||||
.jeecg-modal-content > .scroll-container {
|
||||
padding: 0;
|
||||
|
||||
& > .scrollbar__wrap {
|
||||
overflow: hidden;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="800px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<BasicForm @register="registerForm">
|
||||
<template #typeSlot="{ model, field }">
|
||||
<a-radio-group v-model:value="type" style="display: flex">
|
||||
<a-card
|
||||
v-for="item in appTypeOption"
|
||||
style="margin-right: 10px; cursor: pointer; width: 100%"
|
||||
@click="handleTypeClick(item.value)"
|
||||
:style="type === item.value ? { borderColor: '#3370ff' } : {}"
|
||||
>
|
||||
<a-radio :value="item.value">
|
||||
<div class="type-title">{{ item.title }}</div>
|
||||
<div class="type-desc">{{ item.desc }}</div>
|
||||
</a-radio>
|
||||
</a-card>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { useForm } from '@/components/Form';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { formSchema } from '../AiApp.data';
|
||||
import { initDictOptions } from '@/utils/dict';
|
||||
import { saveApp } from '@/views/super/airag/aiapp/AiApp.api';
|
||||
|
||||
export default {
|
||||
name: 'AiAppModal',
|
||||
components: {
|
||||
BasicForm,
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('创建应用');
|
||||
|
||||
//保存或修改
|
||||
const isUpdate = ref<boolean>(false);
|
||||
|
||||
//app类型
|
||||
const appTypeOption = ref<any>([]);
|
||||
//应用类型
|
||||
const type = ref<string>('chatSimple');
|
||||
|
||||
//表单配置
|
||||
const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
type.value = 'chatSimple';
|
||||
//update-begin---author:wangshuai---date:2025-03-11---for: 【QQYUN-11324】8.修改弹窗head---
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate)) {
|
||||
//表单赋值
|
||||
await setFieldsValue({
|
||||
...data.record,
|
||||
});
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-11---for:【QQYUN-11324】8.修改弹窗head---
|
||||
setModalProps({ minHeight: 500, bodyStyle: { padding: '10px' } });
|
||||
});
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
async function handleOk() {
|
||||
try {
|
||||
let values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
values.type = type.value;
|
||||
let result = await saveApp(values);
|
||||
if (result) {
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
//update-begin---author:wangshuai---date:2025-03-11---for: 【QQYUN-11324】8.修改弹窗head---
|
||||
if(isUpdate.value){
|
||||
//刷新列表
|
||||
emit('success', values);
|
||||
}else{
|
||||
//刷新列表
|
||||
emit('success', result);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-11---for: 【QQYUN-11324】8.修改弹窗head---
|
||||
}
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
//初始化AI应用类型
|
||||
initAppTypeOption();
|
||||
|
||||
function initAppTypeOption() {
|
||||
initDictOptions('ai_app_type').then((data) => {
|
||||
if (data && data.length > 0) {
|
||||
for (const datum of data) {
|
||||
if (datum.value === 'chatSimple') {
|
||||
datum['desc'] = '适合新手创建小助手';
|
||||
} else if (datum.value === 'chatFLow') {
|
||||
datum['desc'] = '适合高级用户自定义小助手的工作流';
|
||||
}
|
||||
}
|
||||
}
|
||||
appTypeOption.value = data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用类型点击事件
|
||||
*/
|
||||
function handleTypeClick(val) {
|
||||
type.value = val;
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
registerForm,
|
||||
title,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
appTypeOption,
|
||||
type,
|
||||
handleTypeClick,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.type-title {
|
||||
color: #1d2025;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.type-desc {
|
||||
color: #8f959e;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,95 @@
|
||||
<!--手动录入text-->
|
||||
<template>
|
||||
<BasicModal title="参数设置" destroyOnClose @register="registerModal" :canFullscreen="false" width="560px" @ok="handleOk" @cancel="handleCancel">
|
||||
<AiModelSeniorForm ref="aiModelSeniorFormRef" :type="type"></AiModelSeniorForm>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModalInner } from '@/components/Modal';
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { MarkdownViewer } from '@/components/Markdown';
|
||||
import AiModelSeniorForm from '/@/views/super/airag/aimodel/components/AiModelSeniorForm.vue';
|
||||
|
||||
export default {
|
||||
name: 'AiAppParamsSettingModal',
|
||||
components: {
|
||||
MarkdownViewer,
|
||||
BasicForm,
|
||||
BasicModal,
|
||||
AiModelSeniorForm,
|
||||
},
|
||||
emits: ['ok', 'register'],
|
||||
setup(props, { emit }) {
|
||||
let aiModelSeniorFormRef = ref()
|
||||
//类型
|
||||
const type = ref<string>('');
|
||||
//注册modal
|
||||
const [registerModal, { closeModal }] = useModalInner(async (data) => {
|
||||
type.value = data.type;
|
||||
if(data.type === 'model'){
|
||||
if(!data.metadata.hasOwnProperty("temperature") ){
|
||||
data.metadata['temperature'] = 0.7;
|
||||
}
|
||||
}else{
|
||||
if(!data.metadata.hasOwnProperty("topNumber") ){
|
||||
data.metadata['topNumber'] = 4;
|
||||
}
|
||||
if(!data.metadata.hasOwnProperty("similarity") ){
|
||||
data.metadata['similarity'] = 0.76;
|
||||
}
|
||||
}
|
||||
setTimeout(()=>{
|
||||
aiModelSeniorFormRef.value.setModalParams(data.metadata);
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* 弹窗点击事件
|
||||
*/
|
||||
function handleOk() {
|
||||
let emitChange = aiModelSeniorFormRef.value.emitChange();
|
||||
emit('ok',emitChange);
|
||||
handleCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗关闭事件
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
type,
|
||||
aiModelSeniorFormRef,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.title-tag {
|
||||
color: #477dee;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="800px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<BasicForm @register="registerForm"></BasicForm>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModalInner } from '@/components/Modal';
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { useForm } from '@/components/Form';
|
||||
import { quickCommandFormSchema} from '../AiApp.data';
|
||||
|
||||
export default {
|
||||
name: 'AiAppQuickCommandModal',
|
||||
components: {
|
||||
BasicForm,
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['ok', 'update-ok', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('添加指令');
|
||||
|
||||
//保存或修改
|
||||
const isUpdate = ref<boolean>(false);
|
||||
|
||||
//表单配置
|
||||
const [registerForm, { validate, resetFields, setFieldsValue }] = useForm({
|
||||
schemas: quickCommandFormSchema,
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
await resetFields();
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
if (unref(isUpdate)) {
|
||||
//表单赋值
|
||||
await setFieldsValue({
|
||||
...data.record,
|
||||
});
|
||||
}
|
||||
setModalProps({ minHeight: 200, bodyStyle: { padding: '10px' } });
|
||||
});
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
async function handleOk() {
|
||||
try {
|
||||
let values = await validate();
|
||||
setModalProps({ confirmLoading: true });
|
||||
if(isUpdate.value){
|
||||
emit('update-ok',values);
|
||||
}else{
|
||||
emit('ok', values);
|
||||
}
|
||||
handleCancel();
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
registerForm,
|
||||
title,
|
||||
handleOk,
|
||||
handleCancel
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.type-title {
|
||||
color: #1d2025;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.type-desc {
|
||||
color: #8f959e;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" :width="width" :title="title" :footer="null">
|
||||
<!-- 嵌入表单 -->
|
||||
<div v-if="type === 'menu'">
|
||||
<a-form layout="vertical" :model="appData">
|
||||
<a-form-item label="菜单名称">
|
||||
<a-input v-model:value="appData.name" readonly/>
|
||||
</a-form-item>
|
||||
<a-form-item label="菜单地址">
|
||||
<a-input v-model:value="appData.menu" readonly/>
|
||||
</a-form-item>
|
||||
<a-form-item style="text-align:right">
|
||||
<a-button @click.prevent="copyMenu">复制菜单</a-button>
|
||||
<a-button type="primary" style="margin-left: 10px" @click="copySql">复制SQL</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
<!-- 嵌入网站 -->
|
||||
<div v-else-if="type === 'web'" class="web">
|
||||
|
||||
<div style="display: flex;margin: 0 auto">
|
||||
<div :class="activeKey===1?'active':''" class="web-img" @click="handleImageClick(1)">
|
||||
<img src="../img/webEmbedded.png" />
|
||||
</div>
|
||||
<div style="margin-left: 10px" :class="activeKey===2?'active':''" class="web-img" @click="handleImageClick(2)">
|
||||
<img src="../img/iconWebEmbedded.png" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="web-title" v-if="activeKey === 1">
|
||||
将以下 iframe 嵌入到你的网站中的目标位置
|
||||
</div>
|
||||
<div class="web-title" v-else>
|
||||
将以下 script 添加到网页的body区域中
|
||||
</div>
|
||||
<div class="web-code" v-if="activeKey === 1">
|
||||
<div class="web-code-title">
|
||||
<div class="web-code-desc">
|
||||
html
|
||||
</div>
|
||||
<Icon class="pointer" icon="ant-design:copy-outlined" @click="copyIframe(1)"></Icon>
|
||||
</div>
|
||||
<div class="web-code-iframe">
|
||||
<pre> {{getIframeText(1)}} </pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="web-code" v-if="activeKey === 2">
|
||||
<div class="web-code-title">
|
||||
<div class="web-code-desc">
|
||||
html
|
||||
</div>
|
||||
<Icon class="pointer" icon="ant-design:copy-outlined" @click="copyIframe(2)"></Icon>
|
||||
</div>
|
||||
<div class="web-code-iframe">
|
||||
<pre> {{getIframeText(2)}} </pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModalInner } from '@/components/Modal';
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { buildUUID } from '@/utils/uuid';
|
||||
import { copyTextToClipboard } from '@/hooks/web/useCopyToClipboard';
|
||||
import { isDevMode } from '/@/utils/env';
|
||||
|
||||
export default {
|
||||
name: 'AiAppSendModal',
|
||||
components: {
|
||||
BasicForm,
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
//标题
|
||||
const title = ref<string>('嵌入网站');
|
||||
const $message = useMessage();
|
||||
//类型
|
||||
const type = ref<string>('web');
|
||||
//应用信息
|
||||
const appData = ref<any>({});
|
||||
//弹窗宽度
|
||||
const width = ref<string>("800px");
|
||||
//选中的key
|
||||
const activeKey = ref<number>(1);
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
type.value = data.type;
|
||||
appData.value = data.data;
|
||||
appData.value.menu = "/ai/chat/"+ data.data.id
|
||||
activeKey.value = 1;
|
||||
let minHeight = 220;
|
||||
if(data.type === 'web'){
|
||||
title.value = '嵌入网站';
|
||||
width.value = '640px';
|
||||
minHeight = 500
|
||||
}else{
|
||||
title.value = '配置菜单';
|
||||
width.value = '500px';
|
||||
}
|
||||
setModalProps({ height: minHeight, bodyStyle: { padding: '10px' } });
|
||||
});
|
||||
|
||||
/**
|
||||
* 复制菜单
|
||||
*/
|
||||
function copyMenu() {
|
||||
copyText(appData.value.menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制sql
|
||||
*/
|
||||
function copySql() {
|
||||
const insertMenuSql = `INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
|
||||
VALUES ('${buildUUID()}', NULL, '${appData.value.name}', '${appData.value.menu}', '1', NULL, NULL, 0, NULL, '1', 0.00, 0, NULL, 0, 1, 0, 0, 0, NULL, '1', 0, 0, 'admin', null, NULL, NULL, 0)`;
|
||||
copyText(insertMenuSql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文本
|
||||
*/
|
||||
function getIframeText(value) {
|
||||
let locationUrl = document.location.protocol +"//" + window.location.host;
|
||||
//update-begin---author:wangshuai---date:2025-03-20---for:【QQYUN-11649】【AI】应用嵌入,支持一个小图标点击出聊天---
|
||||
if(value === 1){
|
||||
return '<iframe\n' +
|
||||
' src="'+locationUrl+'/ai/app/chat/'+appData.value.id+'"\n' +
|
||||
' style="width: 100%; height: 100%;">\n' +
|
||||
'</iframe>';
|
||||
}else{
|
||||
//update-begin---author:wangshuai---date:2025-03-28---for:【QQYUN-11649】应用嵌入,支持一个小图标点击出聊天---
|
||||
let path = "/src/views/super/airag/aiapp/chat/js/chat.js"
|
||||
if(!isDevMode()){
|
||||
path = "/chat/chat.js";
|
||||
}
|
||||
let text ='<script src=' + locationUrl + path +' id="e7e007dd52f67fe36365eff636bbffbd">'+'<'+'/script>';
|
||||
text += '\n <'+'script>\n';
|
||||
text += ' createAiChat({\n' +
|
||||
' appId:"'+ appData.value.id +'",\n';
|
||||
text += ' // 支持top-left左上, top-right右上, bottom-left左下, bottom-right右下\n';
|
||||
text += ' iconPosition:"bottom-right"\n';
|
||||
text += ' })\n';
|
||||
text += ' <'+'/script>';
|
||||
return text;
|
||||
//update-end---author:wangshuai---date:2025-03-28---for:【QQYUN-11649】应用嵌入,支持一个小图标点击出聊天---
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-20---for:【QQYUN-11649】【AI】应用嵌入,支持一个小图标点击出聊天---
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制iframe
|
||||
*/
|
||||
function copyIframe(value) {
|
||||
copyText(getIframeText(value));
|
||||
}
|
||||
|
||||
// 复制文本到剪贴板
|
||||
function copyText(text: string) {
|
||||
const success = copyTextToClipboard(text);
|
||||
if (success) {
|
||||
$message.createMessage.success('复制成功!');
|
||||
} else {
|
||||
$message.createMessage.error('复制失败!');
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片点击事件
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
function handleImageClick(value) {
|
||||
activeKey.value = value;
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
title,
|
||||
type,
|
||||
appData,
|
||||
copySql,
|
||||
copyMenu,
|
||||
width,
|
||||
copyIframe,
|
||||
getIframeText,
|
||||
activeKey,
|
||||
handleImageClick,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.type-title {
|
||||
color: #1d2025;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.type-desc {
|
||||
color: #8f959e;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.web{
|
||||
padding: 0 10px;
|
||||
}
|
||||
.web-title{
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
}
|
||||
.web-img{
|
||||
border-width: 1.5px;
|
||||
width: 240px;
|
||||
margin-top: 20px;
|
||||
border-radius: 6px;
|
||||
img{
|
||||
border-radius: 6px;
|
||||
width: 240px;
|
||||
height: 150px;
|
||||
}
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.active{
|
||||
border-color: rgb(41 112 255);
|
||||
}
|
||||
.web-code{
|
||||
border-width: 1.5px;
|
||||
margin-top: 20px;
|
||||
background-color: #f9fafb;
|
||||
border-color: #10182814;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
.web-code-title{
|
||||
width: 100%;
|
||||
padding:10px;
|
||||
background-color: #f2f4f7;
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.web-code-desc{
|
||||
color: #354052;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
}
|
||||
.web-code-iframe{
|
||||
padding: 15px;
|
||||
line-height: 1.5;
|
||||
font-size: 13px;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
color: #354052;
|
||||
}
|
||||
}
|
||||
.pointer{
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
BIN
jeecgboot-vue3/src/views/super/airag/aiapp/img/ailogo.png
Normal file
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 29 KiB |
BIN
jeecgboot-vue3/src/views/super/airag/aiapp/img/webEmbedded.png
Normal file
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,124 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
enum Api {
|
||||
//知识库管理
|
||||
list = '/airag/knowledge/list',
|
||||
save = '/airag/knowledge/add',
|
||||
delete = '/airag/knowledge/delete',
|
||||
queryById = '/airag/knowledge/queryById',
|
||||
edit = '/airag/knowledge/edit',
|
||||
rebuild = '/airag/knowledge/rebuild',
|
||||
//知识库文档
|
||||
knowledgeDocList = '/airag/knowledge/doc/list',
|
||||
knowledgeEditDoc = '/airag/knowledge/doc/edit',
|
||||
knowledgeDeleteBatchDoc = '/airag/knowledge/doc/deleteBatch',
|
||||
knowledgeRebuildDoc = '/airag/knowledge/doc/rebuild',
|
||||
knowledgeEmbeddingHitTest = '/airag/knowledge/embedding/hitTest',
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询知识库
|
||||
* @param params
|
||||
*/
|
||||
export const list = (params) => {
|
||||
return defHttp.get({ url: Api.list, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据id查询知识库
|
||||
* @param params
|
||||
*/
|
||||
export const queryById = (params) => {
|
||||
return defHttp.get({ url: Api.queryById, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增知识库
|
||||
* @param params
|
||||
*/
|
||||
export const saveKnowledge = (params) => {
|
||||
return defHttp.post({ url: Api.save, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑知识库
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
export const editKnowledge = (params) => {
|
||||
return defHttp.put({ url: Api.edit, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除知识库
|
||||
*/
|
||||
export const deleteModel = (params, handleSuccess) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '是否删除名称为'+params.name+'的知识库吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
return defHttp.delete({ url: Api.delete, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询知识库详情
|
||||
* @param params
|
||||
*/
|
||||
export const knowledgeDocList = (params) => {
|
||||
return defHttp.get({ url: Api.knowledgeDocList, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 知识库向量化
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
export const rebuild = (params) => {
|
||||
return defHttp.put({ url: Api.rebuild, params,timeout: 2 * 60 * 1000 }, { joinParamsToUrl: true, isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增知识库
|
||||
* @param params
|
||||
*/
|
||||
export const knowledgeSaveDoc = (params) => {
|
||||
return defHttp.post({ url: Api.knowledgeEditDoc, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 文档向量化
|
||||
* @param params
|
||||
*/
|
||||
export const knowledgeRebuildDoc = (params, handleSuccess) => {
|
||||
return defHttp.put({ url: Api.knowledgeRebuildDoc, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量删除文档
|
||||
*
|
||||
* @param params
|
||||
* @param handleSuccess
|
||||
*/
|
||||
export const knowledgeDeleteBatchDoc = (params, handleSuccess) => {
|
||||
return defHttp.delete({ url: Api.knowledgeDeleteBatchDoc, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 命中测试
|
||||
* @param params
|
||||
*/
|
||||
export const knowledgeEmbeddingHitTest = (params) => {
|
||||
let url = Api.knowledgeEmbeddingHitTest + '/' + params.knowId;
|
||||
return defHttp.get({ url: url, params }, { isTransformResponse: false });
|
||||
};
|
@ -0,0 +1,125 @@
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { BasicColumn } from '@/components/Table';
|
||||
|
||||
/**
|
||||
* 表单
|
||||
*/
|
||||
export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: 'id',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '知识库名称',
|
||||
field: 'name',
|
||||
required: true,
|
||||
componentProps: {
|
||||
placeholder: '请输入知识库名称',
|
||||
//是否展示字数
|
||||
showCount: true,
|
||||
maxlength: 64,
|
||||
},
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '知识库描述',
|
||||
field: 'descr',
|
||||
component: 'InputTextArea',
|
||||
componentProps: {
|
||||
placeholder: '描述知识库的内容,详尽的描述将帮助AI能深入理解该知识库的内容,能更准确的检索到内容,提高该知识库的命中率。',
|
||||
//是否展示字数
|
||||
showCount: true,
|
||||
maxlength: 256,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '向量模型',
|
||||
field: 'embedId',
|
||||
required: true,
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
dictCode: "airag_model where model_type = 'EMBED',name,id",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
field: 'status',
|
||||
required: true,
|
||||
component: 'JDictSelectTag',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '启用', value: 'enable' },
|
||||
{ label: '禁用', value: 'disable' },
|
||||
],
|
||||
type: 'radioButton',
|
||||
},
|
||||
defaultValue: 'enable',
|
||||
},
|
||||
];
|
||||
|
||||
//文档文本表单
|
||||
export const docTextSchema: FormSchema[] = [
|
||||
{
|
||||
label: 'id',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '知识库id',
|
||||
field: 'knowledgeId',
|
||||
show: false,
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '标题',
|
||||
field: 'title',
|
||||
required: true,
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '类型',
|
||||
field: 'type',
|
||||
required: true,
|
||||
component: 'Input',
|
||||
show: false
|
||||
},
|
||||
{
|
||||
label: '内容',
|
||||
field: 'content',
|
||||
rules: [{ required: true, message: '请输入内容' }],
|
||||
component: 'JMarkdownEditor',
|
||||
componentProps: {
|
||||
placeholder: "请输入内容",
|
||||
preview:{ mode: 'view', action: [] }
|
||||
},
|
||||
ifShow:({ values})=>{
|
||||
if(values.type === 'text'){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '文件',
|
||||
field: 'filePath',
|
||||
rules: [{ required: true, message: '请上传文件' }],
|
||||
component: 'JUpload',
|
||||
helpMessage:'支持txt、markdown、pdf、docx、xlsx、pptx',
|
||||
componentProps:{
|
||||
fileType: 'file',
|
||||
maxCount: 1,
|
||||
multiple: false,
|
||||
text: '上传文档'
|
||||
},
|
||||
ifShow:({ values })=>{
|
||||
if(values.type === 'file'){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
];
|
||||
|
@ -0,0 +1,479 @@
|
||||
<!--知识库添加页面-->
|
||||
<template>
|
||||
<div class="knowledge">
|
||||
<!--查询区域-->
|
||||
<div class="jeecg-basic-table-form-container">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
@keyup.enter.native="searchQuery"
|
||||
:model="queryParam"
|
||||
:label-col="labelCol"
|
||||
:wrapper-col="wrapperCol"
|
||||
style="background-color: #f7f8fc"
|
||||
>
|
||||
<a-row :gutter="24">
|
||||
<a-col :xl="7" :lg="7" :md="8" :sm="24">
|
||||
<a-form-item name="name" label="知识库名称">
|
||||
<JInput v-model:value="queryParam.name" placeholder="请输入知识库名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xl="6" :lg="7" :md="8" :sm="24">
|
||||
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
|
||||
<a-col :lg="6">
|
||||
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
|
||||
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
|
||||
</a-col>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<a-row :span="24" class="knowledge-row">
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
|
||||
<a-card class="add-knowledge-card" @click="handleAddKnowled">
|
||||
<div class="flex">
|
||||
<Icon icon="ant-design:plus-outlined" class="add-knowledge-card-icon" size="20"></Icon>
|
||||
<span class="add-knowledge-card-title">创建知识库</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col v-if="knowledgeList && knowledgeList.length>0" :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" v-for="item in knowledgeList">
|
||||
<a-card class="knowledge-card pointer" @click="handleDocClick(item.id)">
|
||||
<div class="knowledge-header">
|
||||
<div class="flex">
|
||||
<img class="header-img" src="./icon/knowledge.png" />
|
||||
<div class="header-text">
|
||||
<span class="header-text-top header-name ellipsis" :title="item.name"> {{ item.name }} </span>
|
||||
<span class="header-text-top"> 创建者:{{ item.createBy }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-10 text-desc">
|
||||
<span>{{ item.descr || '暂无描述' }}</span>
|
||||
</div>
|
||||
<div class="knowledge-footer">
|
||||
<Icon class="knowledge-footer-icon" icon="ant-design:deployment-unit-outlined" size="14"></Icon>
|
||||
<span>{{ item.embedId_dictText }}</span>
|
||||
</div>
|
||||
<div class="knowledge-btn">
|
||||
<a-dropdown placement="bottomRight" :trigger="['click']" :getPopupContainer="(node) => node.parentNode">
|
||||
<div class="ant-dropdown-link pointer model-icon" @click.prevent.stop>
|
||||
<Icon icon="ant-design:ellipsis-outlined" size="16"></Icon>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="vectorization" @click.prevent.stop="handleVectorization(item.id)">
|
||||
<Icon icon="ant-design:retweet-outlined" size="16"></Icon>
|
||||
向量化
|
||||
</a-menu-item>
|
||||
<a-menu-item key="text" @click.prevent.stop="handleEditClick(item)">
|
||||
<Icon class="pointer" icon="ant-design:edit-outlined" size="16"></Icon>
|
||||
编辑
|
||||
</a-menu-item>
|
||||
<a-menu-item key="file" @click.prevent.stop="handleDelete(item)">
|
||||
<Icon class="pointer" icon="ant-design:delete-outlined" size="16"></Icon>
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<Pagination
|
||||
v-if="knowledgeList.length > 0"
|
||||
:current="pageNo"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:total="total"
|
||||
:showQuickJumper="true"
|
||||
:showSizeChanger="true"
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
/>
|
||||
<!--添加知识库弹窗-->
|
||||
<KnowledgeBaseModal @register="registerModal" @success="reload"></KnowledgeBaseModal>
|
||||
<!-- 知识库文档弹窗 -->
|
||||
<AiragKnowledgeDocListModal @register="docListRegister"></AiragKnowledgeDocListModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
import { deleteModel, list, rebuild } from './AiKnowledgeBase.api';
|
||||
import { Pagination } from 'ant-design-vue';
|
||||
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
|
||||
import KnowledgeBaseModal from './components/AiKnowledgeBaseModal.vue';
|
||||
import JSelectUser from '@/components/Form/src/jeecg/components/JSelectUser.vue';
|
||||
import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
|
||||
import AiragKnowledgeDocListModal from './components/AiragKnowledgeDocListModal.vue';
|
||||
import Icon from '@/components/Icon';
|
||||
import { useMessage } from "@/hooks/web/useMessage";
|
||||
|
||||
export default {
|
||||
name: 'KnowledgeBaseList',
|
||||
components: {
|
||||
Icon,
|
||||
AiragKnowledgeDocListModal,
|
||||
KnowledgeBaseModal,
|
||||
JDictSelectTag,
|
||||
JSelectUser,
|
||||
JInput,
|
||||
Pagination,
|
||||
},
|
||||
setup() {
|
||||
//模型列表
|
||||
const knowledgeList = ref([]);
|
||||
|
||||
//注册modal
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
const [docListRegister, { openModal: openDocModal }] = useModal();
|
||||
|
||||
//当前页数
|
||||
const pageNo = ref<number>(1);
|
||||
//每页条数
|
||||
const pageSize = ref<number>(10);
|
||||
//总条数
|
||||
const total = ref<number>(0);
|
||||
//可选择的页数
|
||||
const pageSizeOptions = ref<any>(['10', '20', '30']);
|
||||
//查询参数
|
||||
const queryParam = reactive<any>({});
|
||||
//查询区域label宽度
|
||||
const labelCol = reactive({
|
||||
xs: 24,
|
||||
sm: 4,
|
||||
xl: 6,
|
||||
xxl: 6,
|
||||
});
|
||||
//查询区域组件宽度
|
||||
const wrapperCol = reactive({
|
||||
xs: 24,
|
||||
sm: 20,
|
||||
});
|
||||
//查询区域表单的ref
|
||||
const formRef = ref();
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
//页面初始化执行列表查询
|
||||
reload();
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
async function handleAddKnowled() {
|
||||
openModal(true, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
function handleEditClick(item) {
|
||||
console.log(item);
|
||||
openModal(true, {
|
||||
id: item.id,
|
||||
isUpdate: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载数据
|
||||
*/
|
||||
function reload() {
|
||||
let params = {
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
column: 'createTime',
|
||||
order: 'desc'
|
||||
};
|
||||
Object.assign(params, queryParam);
|
||||
|
||||
list(params).then((res) => {
|
||||
if (res.success) {
|
||||
knowledgeList.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
} else {
|
||||
knowledgeList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页改变事件
|
||||
* @param page
|
||||
* @param current
|
||||
*/
|
||||
function handlePageChange(page, current) {
|
||||
pageNo.value = page;
|
||||
pageSize.value = current;
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型
|
||||
* @param item
|
||||
*/
|
||||
async function handleDelete(item) {
|
||||
await deleteModel({ id: item.id, name: item.name }, reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
function searchQuery() {
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function searchReset() {
|
||||
formRef.value.resetFields();
|
||||
queryParam.createBy = '';
|
||||
//刷新数据
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数配置点击事件
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
function handleDocClick(id) {
|
||||
openDocModal(true, { id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 知识库向量化
|
||||
* @param id
|
||||
*/
|
||||
async function handleVectorization(id) {
|
||||
rebuild({ knowIds: id }).then((res) =>{
|
||||
if(res.success){
|
||||
createMessage.success("向量化成功!");
|
||||
reload();
|
||||
}else{
|
||||
createMessage.warning("向量化失败!");
|
||||
}
|
||||
}).catch(err=>{
|
||||
createMessage.warning("向量化失败!");
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
handleAddKnowled,
|
||||
handleEditClick,
|
||||
registerModal,
|
||||
knowledgeList,
|
||||
reload,
|
||||
pageNo,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
total,
|
||||
handlePageChange,
|
||||
handleDelete,
|
||||
searchQuery,
|
||||
searchReset,
|
||||
queryParam,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
formRef,
|
||||
handleDocClick,
|
||||
docListRegister,
|
||||
handleVectorization,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.knowledge {
|
||||
height: calc(100vh - 115px);
|
||||
background: #f7f8fc;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
|
||||
.knowledge-row {
|
||||
max-height: calc(100% - 100px);
|
||||
margin-top: 20px;
|
||||
overflow-y: auto;
|
||||
.knowledge-header {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
.header-img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.header-text {
|
||||
width: calc(100% - 80px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: grid;
|
||||
.header-name {
|
||||
font-size: 14px !important;
|
||||
font-weight: bold;
|
||||
color: #354052 !important;
|
||||
}
|
||||
.header-text-top {
|
||||
height: 22px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-desc {
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: #676f83;
|
||||
}
|
||||
|
||||
.knowledge-footer{
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-top: 16px;
|
||||
.knowledge-footer-icon{
|
||||
position: relative;
|
||||
top: 2px
|
||||
}
|
||||
span{
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ant-card .ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.knowledge-btn {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 6px;
|
||||
height: auto;
|
||||
display: none;
|
||||
}
|
||||
.add-knowledge-card {
|
||||
margin-bottom: 20px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
height: 152px;
|
||||
width: calc(100% - 20px);
|
||||
.add-knowledge-card-icon {
|
||||
padding: 8px;
|
||||
color: #1f2329;
|
||||
background-color: #f5f6f7;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.add-knowledge-card-title {
|
||||
font-size: 16px;
|
||||
color:#1f2329;
|
||||
font-weight: 400;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.add-knowledge-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.knowledge-card {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
height: 152px;
|
||||
border-radius: 10px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.knowledge-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
.knowledge-btn {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.list-footer {
|
||||
text-align: right;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.jeecg-basic-table-form-container {
|
||||
padding: 0;
|
||||
:deep(.ant-form) {
|
||||
background-color: transparent;
|
||||
}
|
||||
.table-page-search-submitButtons {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.model-icon{
|
||||
background-color: unset;
|
||||
border: none;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.model-icon:hover{
|
||||
color: #000000;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
border: none;
|
||||
}
|
||||
.ant-dropdown-link{
|
||||
font-size: 14px;
|
||||
height: 24px;
|
||||
padding: 0 7px;
|
||||
border-radius: 4px;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ellipsis{
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<BasicForm @register="registerForm"></BasicForm>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { useForm } from '@/components/Form';
|
||||
import { formSchema } from '../AiKnowledgeBase.data';
|
||||
import { saveKnowledge, editKnowledge, queryById } from '../AiKnowledgeBase.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
export default {
|
||||
name: 'KnowledgeBaseModal',
|
||||
components: {
|
||||
BasicForm,
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('创建知识库');
|
||||
|
||||
//保存或修改
|
||||
const isUpdate = ref<boolean>(false);
|
||||
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, validate, clearValidate, updateSchema }] = useForm({
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
//重置表单
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
title.value = isUpdate.value ? '编辑知识库' : '创建知识库';
|
||||
if (unref(isUpdate)) {
|
||||
let values = await queryById({ id: data.id });
|
||||
//表单赋值
|
||||
await setFieldsValue({
|
||||
...values.result,
|
||||
});
|
||||
}
|
||||
setModalProps({ minHeight: 500, bodyStyle: { padding: '10px' } });
|
||||
});
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
async function handleOk() {
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
let values = await validate();
|
||||
if (!unref(isUpdate)) {
|
||||
await saveKnowledge(values);
|
||||
} else {
|
||||
await editKnowledge(values);
|
||||
}
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
//刷新列表
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
registerForm,
|
||||
title,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,70 @@
|
||||
<!--手动录入text-->
|
||||
<template>
|
||||
<BasicModal title="段落详情" destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" :footer="null">
|
||||
<div class="p-2">
|
||||
<div class="header">
|
||||
<a-tag color="#a9c8ff">
|
||||
<span>{{hitTextDescData.source}}</span>
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="content">
|
||||
<MarkdownViewer :value="hitTextDescData.content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModalInner } from '@/components/Modal';
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { MarkdownViewer } from '@/components/Markdown';
|
||||
|
||||
export default {
|
||||
name: 'AiTextDescModal',
|
||||
components: {
|
||||
MarkdownViewer,
|
||||
BasicForm,
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
let hitTextDescData = ref<any>({})
|
||||
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
hitTextDescData.value.source = 'score' + ' ' + data.score.toFixed(2);
|
||||
hitTextDescData.value.content = data.content;
|
||||
setModalProps({ header: '300px' })
|
||||
});
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
hitTextDescData
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.header {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.title-tag {
|
||||
color: #477dee;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,900 @@
|
||||
<!--知识库文档列表-->
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal
|
||||
wrapClassName="airag-knowledge-doc"
|
||||
destroyOnClose
|
||||
@register="registerModal"
|
||||
:canFullscreen="false"
|
||||
defaultFullscreen
|
||||
:title="title"
|
||||
:footer="null"
|
||||
>
|
||||
<a-layout style="height: 100%">
|
||||
<a-layout-sider :style="siderStyle">
|
||||
<a-menu v-model:selectedKeys="selectedKeys" mode="vertical" style="border: none" :items="menuItems" @click="handleMenuClick" />
|
||||
</a-layout-sider>
|
||||
<a-layout-content :style="contentStyle">
|
||||
<div v-if="selectedKey === 'document'">
|
||||
<a-input v-model:value="searchText" placeholder="请输入文档名称,回车搜索" class="search-title" @pressEnter="reload"/>
|
||||
<a-row :span="24" class="knowledge-row">
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
|
||||
<a-card class="add-knowledge-card" :bodyStyle="cardBodyStyle">
|
||||
<span style="line-height: 18px;font-weight: 500;color:#676f83;font-size: 12px">创建文档</span>
|
||||
<div class="add-knowledge-doc" @click="handleCreateText">
|
||||
<Icon icon="ant-design:form-outlined" size="13"></Icon><span>手动录入</span>
|
||||
</div>
|
||||
<div class="add-knowledge-doc" @click="handleCreateUpload">
|
||||
<Icon icon="ant-design:cloud-upload-outlined" size="13"></Icon><span>文件上传</span>
|
||||
</div>
|
||||
<div class="add-knowledge-doc" @click="handleCreateUploadLibrary">
|
||||
<a-upload
|
||||
accept=".zip"
|
||||
name="file"
|
||||
:data="{ knowId: knowledgeId }"
|
||||
:showUploadList="false"
|
||||
:headers="headers"
|
||||
:beforeUpload="beforeUpload"
|
||||
:action="uploadUrl"
|
||||
@change="handleUploadChange"
|
||||
>
|
||||
<Icon style="margin-left: 0" icon="ant-design:project-outlined" size="13"></Icon>
|
||||
<span>文档库上传</span>
|
||||
</a-upload>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" v-for="item in knowledgeDocDataList">
|
||||
<a-card class="knowledge-card pointer" @click="handleEdit(item)">
|
||||
<div class="knowledge-header">
|
||||
<div class="header-text flex">
|
||||
<Icon v-if="item.type==='text'" icon="ant-design:file-text-outlined" size="32" color="#00a7d0"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === 'pdf'" icon="ant-design:file-pdf-outlined" size="32" color="rgb(211, 47, 47)"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === 'docx'" icon="ant-design:file-word-outlined" size="32" color="rgb(68, 138, 255)"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === 'pptx'" icon="ant-design:file-ppt-outlined" size="32" color="rgb(245, 124, 0)"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === 'xlsx'" icon="ant-design:file-excel-outlined" size="32" color="rgb(98, 187, 55)"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === 'txt'" icon="ant-design:file-text-outlined" size="32" color="#00a7d0"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === 'md'" icon="ant-design:file-markdown-outlined" size="32" color="#292929"></Icon>
|
||||
<Icon v-if="item.type==='file' && getFileSuffix(item.metadata) === ''" icon="ant-design:file-unknown-outlined" size="32" color="#f5f5dc"></Icon>
|
||||
<span class="ellipsis header-title">{{ item.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-description">
|
||||
<span>{{ item.content }}</span>
|
||||
</div>
|
||||
<div class="flex" style="justify-content: space-between">
|
||||
<div class="card-text">
|
||||
状态:
|
||||
<div v-if="item.status==='complete'" class="card-text-status">
|
||||
<Icon icon="ant-design:check-circle-outlined" size="16" color="#56D1A7"></Icon>
|
||||
<span class="ml-2">已完成</span>
|
||||
</div>
|
||||
<div v-else-if="item.status==='building'" class="card-text-status">
|
||||
<a-spin v-if="item.loading" :spinning="item.loading" :indicator="indicator"></a-spin>
|
||||
<span class="ml-2">构建中</span>
|
||||
</div>
|
||||
<div v-else-if="item.status==='draft'" class="card-text-status">
|
||||
<img src="../icon/draft.png" style="width: 16px;height: 16px" />
|
||||
<span class="ml-2">草稿</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-dropdown placement="bottomRight" :trigger="['click']">
|
||||
<div class="ant-dropdown-link pointer operation" @click.prevent.stop>
|
||||
<Icon icon="ant-design:ellipsis-outlined" size="16"></Icon>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="vectorization" @click="handleVectorization(item.id)">
|
||||
<Icon icon="ant-design:retweet-outlined" size="16"></Icon>
|
||||
向量化
|
||||
</a-menu-item>
|
||||
<a-menu-item key="edit" @click="handleEdit(item)">
|
||||
<Icon icon="ant-design:edit-outlined" size="16"></Icon>
|
||||
编辑
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete" @click="handleDelete(item.id)">
|
||||
<Icon icon="ant-design:delete-outlined" size="16"></Icon>
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<Pagination
|
||||
v-if="knowledgeDocDataList.length > 0"
|
||||
:current="pageNo"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:total="total"
|
||||
:showQuickJumper="true"
|
||||
:showSizeChanger="true"
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedKey === 'hitTest'" style="padding: 16px">
|
||||
<a-spin :spinning="spinning">
|
||||
<div class="hit-test">
|
||||
<h4>命中测试</h4>
|
||||
<span>针对用户提问调试段落匹配情况,保障回答效果。</span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="content-title">
|
||||
<Avatar v-if="hitShowSearchText" :size="35" :src="avatar" />
|
||||
<span>{{ hitShowSearchText }}</span>
|
||||
</div>
|
||||
<div class="content-card">
|
||||
<a-row :span="24" class="knowledge-row" v-if="hitTextList.length>0">
|
||||
<a-col :xxl="6" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" v-for="item in hitTextList">
|
||||
<a-card class="hit-card pointer" style="border-color: #ffffff" @click="hitTextDescClick(item)">
|
||||
<div class="card-title">
|
||||
<div style="display: flex;">
|
||||
<Icon icon="ant-design:appstore-outlined" size="14"></Icon>
|
||||
<span style="margin-left: 4px">Chunk-{{item.chunk}}</span>
|
||||
<span style="margin-left: 10px">{{ item.content.length }} 字符</span>
|
||||
</div>
|
||||
<a-tag class="card-title-tag" color="#a9c8ff">
|
||||
<span>{{ getTagTxt(item.score) }}</span>
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="card-description">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{{item.docName}}
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div v-else-if="notHit">
|
||||
<a-empty :image-style="{ margin: '0 auto', height: '160px', verticalAlign: 'middle', borderStyle: 'none' }">
|
||||
<template #description>
|
||||
<div style="margin-top: 26px; font-size: 20px; color: #000; text-align: center !important">
|
||||
没有命中的分段
|
||||
</div>
|
||||
</template>
|
||||
</a-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param">
|
||||
<span style="font-weight: bold; font-size: 16px">参数配置</span>
|
||||
<ul>
|
||||
<li>
|
||||
<span>条数:</span>
|
||||
<a-input-number :min="1" v-model:value="topNumber"></a-input-number>
|
||||
</li>
|
||||
<li>
|
||||
<span>Score阈值:</span>
|
||||
<a-input-number :min="0" :step="0.01" :max="1" v-model:value="similarity"></a-input-number>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="hit-test-footer">
|
||||
<a-input v-model:value="hitText" size="large" placeholder="请输入" style="width: 100%" @pressEnter="hitTestClick">
|
||||
<template #suffix>
|
||||
<Icon icon="ant-design:send-outlined" style="transform: rotate(-33deg); cursor: pointer" size="22" @click="hitTestClick"></Icon>
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</BasicModal>
|
||||
|
||||
<!-- 手工录入文本 -->
|
||||
<AiragKnowledgeDocTextModal @register="docTextRegister" @success="handleSuccess"></AiragKnowledgeDocTextModal>
|
||||
<!-- 文本明细 -->
|
||||
<AiTextDescModal @register="docTextDescRegister"></AiTextDescModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { onBeforeMount, reactive, ref, unref, h } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
import { knowledgeDocList, knowledgeDeleteBatchDoc, knowledgeRebuildDoc, knowledgeEmbeddingHitTest } from '../AiKnowledgeBase.api';
|
||||
import { ActionItem, BasicTable, TableAction } from '@/components/Table';
|
||||
import { useListPage } from '@/hooks/system/useListPage';
|
||||
import AiragKnowledgeDocTextModal from './AiragKnowledgeDocTextModal.vue';
|
||||
import AiTextDescModal from './AiTextDescModal.vue';
|
||||
import { useMessage } from '@/hooks/web/useMessage';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import {Avatar, message, Modal, Pagination} from 'ant-design-vue';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { getFileAccessHttpUrl, getHeaders } from '@/utils/common/compUtils';
|
||||
import defaultImg from '/@/assets/images/header.jpg';
|
||||
import Icon from "@/components/Icon";
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
|
||||
export default {
|
||||
name: 'AiragKnowledgeDocListModal',
|
||||
components: {
|
||||
Icon,
|
||||
Pagination,
|
||||
Avatar,
|
||||
LoadingOutlined,
|
||||
TableAction,
|
||||
BasicTable,
|
||||
BasicModal,
|
||||
AiragKnowledgeDocTextModal,
|
||||
AiTextDescModal,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
//标题
|
||||
const title = ref<string>('知识库详情');
|
||||
|
||||
//保存或修改
|
||||
const knowledgeId = ref<string>('');
|
||||
|
||||
//菜单初始化选中的值
|
||||
const selectedKeys = ref(['document']);
|
||||
//菜单点击选中的key,用于显示和隐藏table
|
||||
const selectedKey = ref<string>('document');
|
||||
//定向搜索的文本
|
||||
const hitText = ref<string>('');
|
||||
//定向显示的文本
|
||||
const hitShowSearchText = ref<string>('');
|
||||
//加载效果
|
||||
const spinning = ref<boolean>(false);
|
||||
//最小分数 0-1
|
||||
const similarity = ref<number>(0.65);
|
||||
//条数
|
||||
const topNumber = ref<number>(5);
|
||||
//定向返回结果集
|
||||
const hitTextList = ref<any>([]);
|
||||
//用户头像
|
||||
const avatar = ref<any>('');
|
||||
const userStore = useUserStore();
|
||||
//文档列表
|
||||
const knowledgeDocDataList = ref<any>([]);
|
||||
//当前页数
|
||||
const pageNo = ref<number>(1);
|
||||
//每页条数
|
||||
const pageSize = ref<number>(10);
|
||||
//总条数
|
||||
const total = ref<number>(0);
|
||||
//可选择的页数
|
||||
const pageSizeOptions = ref<any>(['10', '20', '30']);
|
||||
//查询参数
|
||||
const searchText = ref<string>('');
|
||||
//是否没有命中
|
||||
const notHit = ref<boolean>(false);
|
||||
//定时任务刷新列表
|
||||
const timer = ref<any>(null);
|
||||
//token
|
||||
const headers = getHeaders();
|
||||
const globSetting = useGlobSetting();
|
||||
//上传路径
|
||||
const uploadUrl = ref<string>(globSetting.domainUrl+"/airag/knowledge/doc/import/zip");
|
||||
|
||||
//菜单项
|
||||
const menuItems = ref<any>([
|
||||
{
|
||||
key: 'document',
|
||||
icon: '',
|
||||
label: '文档',
|
||||
title: '文档',
|
||||
},
|
||||
{
|
||||
key: 'hitTest',
|
||||
icon: '',
|
||||
label: '命中测试',
|
||||
title: '命中测试',
|
||||
},
|
||||
]);
|
||||
//注册modal
|
||||
const [docTextRegister, { openModal: docTextOpenModal }] = useModal();
|
||||
const [docTextDescRegister, { openModal: docTextDescOpenModal }] = useModal();
|
||||
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
knowledgeId.value = data.id;
|
||||
selectedKeys.value = ['document'];
|
||||
selectedKey.value = 'document';
|
||||
spinning.value = false;
|
||||
notHit.value = false;
|
||||
await reload();
|
||||
setModalProps({ confirmLoading: false });
|
||||
});
|
||||
|
||||
const contentStyle = {
|
||||
textAlign: 'center',
|
||||
height: '100%',
|
||||
width: '80%',
|
||||
background: '#ffffff',
|
||||
};
|
||||
|
||||
const siderStyle = {
|
||||
textAlign: 'center',
|
||||
width: '20%',
|
||||
background: '#ffffff',
|
||||
borderRight: '1px solid #cecece',
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载指示符
|
||||
*/
|
||||
const indicator = h(LoadingOutlined, {
|
||||
style: {
|
||||
fontSize: '16px',
|
||||
marginRight: '2px'
|
||||
},
|
||||
spin: true,
|
||||
});
|
||||
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
/**
|
||||
* 手工录入文本
|
||||
*/
|
||||
function handleCreateText() {
|
||||
docTextOpenModal(true, { knowledgeId: knowledgeId.value, type: "text" });
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
*/
|
||||
function handleCreateUpload() {
|
||||
docTextOpenModal(true, { knowledgeId: knowledgeId.value, type: "file" });
|
||||
}
|
||||
|
||||
/**
|
||||
* web网络地址
|
||||
*/
|
||||
function handleCreateWeb() {
|
||||
createMessage.warning('功能正在完善中....');
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
function handleEdit(record) {
|
||||
if (record.type === 'text' || record.type === 'file') {
|
||||
docTextOpenModal(true, {
|
||||
record,
|
||||
isUpdate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param id
|
||||
*/
|
||||
function handleDelete(id) {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认要删除该文档吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
knowledgeDeleteBatchDoc({ ids: id }, reload);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量化
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
async function handleVectorization(id) {
|
||||
await knowledgeRebuildDoc({ docIds: id }, handleSuccess);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档新增和编辑成功回调
|
||||
*/
|
||||
function handleSuccess() {
|
||||
if(!timer.value){
|
||||
reload();
|
||||
}
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
triggeringTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发定时任务
|
||||
*/
|
||||
function triggeringTimer() {
|
||||
timer.value = setInterval(() => {
|
||||
reload();
|
||||
},5000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单点击事件
|
||||
* @param value
|
||||
*/
|
||||
function handleMenuClick(value) {
|
||||
if (value.key === 'document') {
|
||||
setTimeout(() => {
|
||||
pageNo.value = 1;
|
||||
pageSize.value = 10;
|
||||
searchText.value = "";
|
||||
|
||||
reload();
|
||||
});
|
||||
} else {
|
||||
hitTextList.value = [];
|
||||
hitShowSearchText.value = '';
|
||||
hitText.value = '';
|
||||
avatar.value = '';
|
||||
similarity.value = 0.65;
|
||||
topNumber.value = 5;
|
||||
}
|
||||
selectedKey.value = value.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命中测试
|
||||
*/
|
||||
function hitTestClick() {
|
||||
if (hitText.value) {
|
||||
spinning.value = true;
|
||||
knowledgeEmbeddingHitTest({
|
||||
queryText: hitText.value,
|
||||
knowId: knowledgeId.value,
|
||||
topNumber: topNumber.value,
|
||||
similarity: similarity.value,
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
if (res.result) {
|
||||
hitTextList.value = res.result;
|
||||
} else {
|
||||
hitTextList.value = [];
|
||||
}
|
||||
}
|
||||
hitShowSearchText.value = hitText.value;
|
||||
avatar.value = userStore.getUserInfo.avatar ? getFileAccessHttpUrl(userStore.getUserInfo.avatar) : defaultImg;
|
||||
hitText.value = '';
|
||||
notHit.value = hitTextList.value.length == 0;
|
||||
spinning.value = false;
|
||||
}).catch(()=>{
|
||||
spinning.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文本
|
||||
* @param value
|
||||
*/
|
||||
function getTagTxt(value) {
|
||||
return 'score' + ' ' + value.toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 命中测试卡点击事件
|
||||
* @param values
|
||||
*/
|
||||
function hitTextDescClick(values) {
|
||||
docTextDescOpenModal(true, { ...values });
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载表格
|
||||
*/
|
||||
async function reload() {
|
||||
let params = {
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
knowledgeId: knowledgeId.value,
|
||||
title: '*' + searchText.value + '*',
|
||||
column: 'createTime',
|
||||
order: 'desc'
|
||||
};
|
||||
await knowledgeDocList(params).then((res) => {
|
||||
if (res.success) {
|
||||
//update-begin---author:wangshuai---date:2025-03-21---for:【QQYUN-11636】向量化功能改成异步---
|
||||
if(res.result.records){
|
||||
let clearTimer = true;
|
||||
for (const item of res.result.records) {
|
||||
if(item.status && item.status === 'building' ){
|
||||
clearTimer = false;
|
||||
item.loading = true;
|
||||
}else{
|
||||
item.loading = false;
|
||||
}
|
||||
}
|
||||
if(clearTimer){
|
||||
clearInterval(timer.value);
|
||||
}
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-21---for:【QQYUN-11636】向量化功能改成异步---
|
||||
knowledgeDocDataList.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
} else {
|
||||
knowledgeDocDataList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页改变事件
|
||||
* @param page
|
||||
* @param current
|
||||
*/
|
||||
function handlePageChange(page, current) {
|
||||
pageNo.value = page;
|
||||
pageSize.value = current;
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件后缀
|
||||
*/
|
||||
function getFileSuffix(metadata) {
|
||||
if(metadata){
|
||||
let filePath = JSON.parse(metadata).filePath;
|
||||
const index = filePath.lastIndexOf('.');
|
||||
return index > 0 ? filePath.substring(index + 1).toLowerCase() : '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前事件
|
||||
*/
|
||||
function beforeUpload(file) {
|
||||
let fileType = file.type;
|
||||
if (fileType !== 'application/x-zip-compressed') {
|
||||
createMessage.warning('请上传zip文件');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传回调事件
|
||||
* @param info
|
||||
*/
|
||||
function handleUploadChange(info) {
|
||||
let { file } = info;
|
||||
if (file.status === 'error') {
|
||||
createMessage.error(file.response.message ||`${file.name} 上传失败.`);
|
||||
}
|
||||
if (file.status === 'done') {
|
||||
if(!file.response.success){
|
||||
createMessage.warning(file.response.message);
|
||||
return;
|
||||
}
|
||||
createMessage.success(file.response.message);
|
||||
handleSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(()=>{
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
})
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
title,
|
||||
docTextRegister,
|
||||
handleCreateText,
|
||||
beforeUpload,
|
||||
handleCreateUpload,
|
||||
handleSuccess,
|
||||
contentStyle,
|
||||
siderStyle,
|
||||
selectedKeys,
|
||||
menuItems,
|
||||
handleMenuClick,
|
||||
selectedKey,
|
||||
hitTestClick,
|
||||
hitText,
|
||||
spinning,
|
||||
similarity,
|
||||
topNumber,
|
||||
hitShowSearchText,
|
||||
avatar,
|
||||
hitTextList,
|
||||
getTagTxt,
|
||||
docTextDescRegister,
|
||||
hitTextDescClick,
|
||||
knowledgeDocDataList,
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
handleVectorization,
|
||||
pageNo,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
total,
|
||||
handlePageChange,
|
||||
searchText,
|
||||
reload,
|
||||
cardBodyStyle:{ textAlign: 'left', width: '100%' },
|
||||
getFileSuffix,
|
||||
notHit,
|
||||
indicator,
|
||||
headers,
|
||||
uploadUrl,
|
||||
handleUploadChange,
|
||||
knowledgeId,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hit-test {
|
||||
box-sizing: border-box;
|
||||
flex-wrap: wrap;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
||||
h4 {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
align-self: center;
|
||||
margin-bottom: 0;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 10px;
|
||||
color: #8f959e;
|
||||
font-weight: 400;
|
||||
align-self: center;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.hit-test-footer {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
}
|
||||
.param {
|
||||
text-align: left;
|
||||
margin-top: 10px;
|
||||
ul {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
li {
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
border-bottom: 1px solid #cec6c6;
|
||||
}
|
||||
.content {
|
||||
height: calc(100vh - 300px);
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
background-color: #f9fbfd;
|
||||
overflow-y: auto;
|
||||
.content-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
span {
|
||||
margin-left: 4px;
|
||||
font-size: 20px;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
.content-card {
|
||||
margin-top: 20px;
|
||||
margin-left: 10px;
|
||||
.hit-card {
|
||||
height: 160px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 10px;
|
||||
border-radius: 10px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
.card-title {
|
||||
justify-content: space-between;
|
||||
color: #887a8b;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.hit-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15) !important;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
height: 6em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.5;
|
||||
margin-top: 16px;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: #676F83;
|
||||
}
|
||||
|
||||
.card-title-tag {
|
||||
color: #477dee;
|
||||
}
|
||||
|
||||
.knowledge-row {
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.add-knowledge-card {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: inline-flex;
|
||||
font-size: 16px;
|
||||
height: 166px;
|
||||
width: calc(100% - 20px);
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
.add-knowledge-card-icon {
|
||||
padding: 8px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-card {
|
||||
border-radius: 10px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
height: 166px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
.knowledge-header {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
height: 20px;
|
||||
text-align: left;
|
||||
.header-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.header-title{
|
||||
font-weight: bold;
|
||||
color: #354052;
|
||||
margin-left: 4px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-knowledge-card,.knowledge-card{
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.add-knowledge-card:hover,.knowledge-card:hover{
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
:deep(.ant-card .ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-text{
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-title{
|
||||
width: 200px;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.operation{
|
||||
border: none;
|
||||
margin-top: 10px;
|
||||
align-items: end;
|
||||
display: none !important;
|
||||
bottom: 8px;
|
||||
right: 4px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.knowledge-card:hover{
|
||||
.operation{
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.add-knowledge-doc{
|
||||
margin-top: 6px;
|
||||
color:#6F6F83;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
span{
|
||||
margin-left: 4px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
.add-knowledge-doc:hover{
|
||||
background: #c8ceda33;
|
||||
}
|
||||
.operation{
|
||||
background-color: unset;
|
||||
border: none;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.operation:hover{
|
||||
color: #000000;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
border: none;
|
||||
}
|
||||
.ant-dropdown-link{
|
||||
font-size: 14px;
|
||||
height: 24px;
|
||||
padding: 0 7px;
|
||||
border-radius: 4px;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.card-footer{
|
||||
margin-top: 4px;
|
||||
font-weight: 400;
|
||||
color: #1f2329;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
}
|
||||
.card-text-status{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ml-2{
|
||||
margin-left: 2px;
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.airag-knowledge-doc .scroll-container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,111 @@
|
||||
<!--手动录入text-->
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<BasicModal destroyOnClose @register="registerModal" width="600px" :title="title" @ok="handleOk" @cancel="handleCancel">
|
||||
<BasicForm @register="registerForm"></BasicForm>
|
||||
</BasicModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, unref } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { useForm } from '@/components/Form';
|
||||
import { docTextSchema } from '../AiKnowledgeBase.data';
|
||||
import { knowledgeSaveDoc, queryById } from '../AiKnowledgeBase.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
|
||||
export default {
|
||||
name: 'AiragKnowledgeDocModal',
|
||||
components: {
|
||||
BasicForm,
|
||||
BasicModal,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('创建知识库');
|
||||
|
||||
//保存或修改
|
||||
const isUpdate = ref<boolean>(false);
|
||||
//知识库id
|
||||
const knowledgeId = ref<string>();
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, validate, clearValidate, updateSchema }] = useForm({
|
||||
schemas: docTextSchema,
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
//重置表单
|
||||
await resetFields();
|
||||
setModalProps({ confirmLoading: false });
|
||||
isUpdate.value = !!data?.isUpdate;
|
||||
title.value = isUpdate.value ? '编辑文档' : '创建文档';
|
||||
if (unref(isUpdate)) {
|
||||
if(data.record.type === 'file' && data.record.metadata){
|
||||
data.record.filePath = JSON.parse(data.record.metadata).filePath;
|
||||
}
|
||||
//表单赋值
|
||||
await setFieldsValue({
|
||||
...data.record,
|
||||
});
|
||||
} else {
|
||||
knowledgeId.value = data.knowledgeId;
|
||||
await setFieldsValue({ type: data.type })
|
||||
}
|
||||
setModalProps({ bodyStyle: { padding: '10px' } });
|
||||
});
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
async function handleOk() {
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
let values = await validate();
|
||||
if (!unref(isUpdate)) {
|
||||
values.knowledgeId = knowledgeId.value;
|
||||
}
|
||||
if(values.filePath){
|
||||
values.metadata = JSON.stringify({ filePath: values.filePath });
|
||||
delete values.filePath;
|
||||
}
|
||||
await knowledgeSaveDoc(values);
|
||||
//关闭弹窗
|
||||
closeModal();
|
||||
//刷新列表
|
||||
emit('success');
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
registerForm,
|
||||
title,
|
||||
handleOk,
|
||||
handleCancel,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
BIN
jeecgboot-vue3/src/views/super/airag/aiknowledge/icon/draft.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 10 KiB |
433
jeecgboot-vue3/src/views/super/airag/aimodel/AiModelList.vue
Normal file
@ -0,0 +1,433 @@
|
||||
<template>
|
||||
<div class="model">
|
||||
<!--查询区域-->
|
||||
<div class="jeecg-basic-table-form-container">
|
||||
<a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol" style="background-color: #f7f8fc !important;">
|
||||
<a-row :gutter="24">
|
||||
<a-col :lg="6">
|
||||
<a-form-item name="name" label="模板名称">
|
||||
<JInput v-model:value="queryParam.name" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :lg="6">
|
||||
<a-form-item name="modelType" label="模板类型">
|
||||
<JDictSelectTag v-model:value="queryParam.modelType" dict-code="model_type" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xl="6" :lg="7" :md="8" :sm="24">
|
||||
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
|
||||
<a-col :lg="6">
|
||||
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
|
||||
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
|
||||
</a-col>
|
||||
</span>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
<a-row :span="24" class="model-row">
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24">
|
||||
<a-card class="add-knowledge-card" @click="handleAdd">
|
||||
<div class="flex">
|
||||
<Icon icon="ant-design:plus-outlined" class="add-knowledge-card-icon" size="20"></Icon>
|
||||
<span class="add-knowledge-card-title">添加模型</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xxl="4" :xl="6" :lg="6" :md="6" :sm="12" :xs="24" v-for="item in modalList" v-if="modalList && modalList.length>0">
|
||||
<a-card class="model-card" @click="handleEditClick(item)">
|
||||
<div class="model-header">
|
||||
<div class="flex">
|
||||
<img :src="getImage(item.provider)" class="header-img" />
|
||||
<div class="header-text">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<ul>
|
||||
<li class="flex mr-14">
|
||||
<span class="label">模型类型</span>
|
||||
<span class="described">{{ item.modelType_dictText }}</span>
|
||||
</li>
|
||||
<li class="flex mr-14 mt-6">
|
||||
<span class="label">基础模型</span>
|
||||
<span class="described">{{ item.modelName }}</span>
|
||||
</li>
|
||||
<li class="flex mr-14 mt-6">
|
||||
<span class="label">创建者</span>
|
||||
<span class="described">{{ item.createBy }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="model-btn">
|
||||
<a-button class="model-icon" size="small" @click.prevent.stop="handleEditClick(item)">
|
||||
<Icon icon="ant-design:edit-outlined"></Icon>
|
||||
</a-button>
|
||||
<a-dropdown placement="bottomRight" :trigger="['click']" :getPopupContainer="(node) => node.parentNode">
|
||||
<div class="ant-dropdown-link pointer model-icon" @click.prevent.stop>
|
||||
<Icon icon="ant-design:ellipsis-outlined"></Icon>
|
||||
</div>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<!--<a-menu-item key="param" @click="handleParamClick(item.id)">
|
||||
<Icon icon="ant-design:setting-outlined" size="16"></Icon>
|
||||
<span class="ml-4">模型参数配置</span>
|
||||
</a-menu-item>-->
|
||||
<a-menu-item key="delete" @click.prevent.stop="handleDeleteClick(item)">
|
||||
<Icon icon="ant-design:delete-outlined" size="16"></Icon> 删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<Pagination
|
||||
v-if="modalList.length > 0"
|
||||
:current="pageNo"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
:total="total"
|
||||
:showQuickJumper="true"
|
||||
:showSizeChanger="true"
|
||||
@change="handlePageChange"
|
||||
class="list-footer"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!--添加模型弹窗-->
|
||||
<AiModelModal @register="registerModal" @success="reload"></AiModelModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import AiModelModal from './components/AiModelModal.vue';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import { deleteModel, list } from './model.api';
|
||||
import { imageList } from './model.data';
|
||||
import { Pagination } from 'ant-design-vue';
|
||||
import JInput from '@/components/Form/src/jeecg/components/JInput.vue';
|
||||
import JSelectUser from '@/components/Form/src/jeecg/components/JSelectUser.vue';
|
||||
import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
|
||||
|
||||
export default {
|
||||
name: 'ModelList',
|
||||
components: {
|
||||
JDictSelectTag,
|
||||
JSelectUser,
|
||||
JInput,
|
||||
AiModelModal,
|
||||
Pagination,
|
||||
},
|
||||
setup() {
|
||||
//模型列表
|
||||
const modalList = ref([]);
|
||||
|
||||
const [registerModal, { openModal }] = useModal();
|
||||
|
||||
//当前页数
|
||||
const pageNo = ref<number>(1);
|
||||
//每页条数
|
||||
const pageSize = ref<number>(10);
|
||||
//总条数
|
||||
const total = ref<number>(0);
|
||||
//可选择的页数
|
||||
const pageSizeOptions = ref<any>(['10', '20', '30']);
|
||||
//查询参数
|
||||
const queryParam = reactive<any>({});
|
||||
//查询区域label宽度
|
||||
const labelCol = reactive({
|
||||
xs: 24,
|
||||
sm: 4,
|
||||
xl: 6,
|
||||
xxl: 6,
|
||||
});
|
||||
//查询区域组件宽度
|
||||
const wrapperCol = reactive({
|
||||
xs: 24,
|
||||
sm: 20,
|
||||
});
|
||||
//查询区域表单的ref
|
||||
const formRef = ref();
|
||||
|
||||
//页面初始化执行列表查询
|
||||
reload();
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
async function handleAdd() {
|
||||
openModal(true, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
function handleEditClick(item) {
|
||||
openModal(true, {
|
||||
id: item.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载数据
|
||||
*/
|
||||
function reload() {
|
||||
let params = {
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
column: 'createTime',
|
||||
order: 'desc'
|
||||
};
|
||||
Object.assign(params, queryParam);
|
||||
list(params).then((res) => {
|
||||
if (res.success) {
|
||||
modalList.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
} else {
|
||||
modalList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页改变事件
|
||||
* @param page
|
||||
* @param current
|
||||
*/
|
||||
function handlePageChange(page, current) {
|
||||
pageNo.value = page;
|
||||
pageSize.value = current;
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片
|
||||
* @param name
|
||||
*/
|
||||
const getImage = (name) => {
|
||||
return imageList.value[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除模型
|
||||
* @param item
|
||||
*/
|
||||
async function handleDeleteClick(item) {
|
||||
await deleteModel({ id: item.id, name: item.name }, reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询
|
||||
*/
|
||||
function searchQuery() {
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
function searchReset() {
|
||||
formRef.value.resetFields();
|
||||
queryParam.createBy = '';
|
||||
//刷新数据
|
||||
reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数配置点击事件
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
function handleParamClick(id) {}
|
||||
|
||||
return {
|
||||
handleAdd,
|
||||
handleEditClick,
|
||||
registerModal,
|
||||
modalList,
|
||||
reload,
|
||||
pageNo,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
total,
|
||||
handlePageChange,
|
||||
getImage,
|
||||
handleDeleteClick,
|
||||
searchQuery,
|
||||
searchReset,
|
||||
queryParam,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
formRef,
|
||||
handleParamClick,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.model {
|
||||
height: calc(100vh - 115px);
|
||||
background: #f7f8fc;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
.model-row {
|
||||
max-height: calc(100% - 100px);
|
||||
margin-top: 20px;
|
||||
overflow-y: auto;
|
||||
.model-header {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
.header-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.header-text {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #354052;
|
||||
width: calc(100% - 80px);
|
||||
overflow: hidden;
|
||||
align-content: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
align-self: center;
|
||||
color: #8a8f98;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.described {
|
||||
font-weight: 400;
|
||||
margin-left: 14px;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:deep(.ant-card .ant-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mr-14 {
|
||||
margin-right: 14px;
|
||||
}
|
||||
.mt-6 {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.model-btn {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 6px;
|
||||
height: auto;
|
||||
display: none;
|
||||
}
|
||||
.model-card {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 10px;
|
||||
height: 152px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.model-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
.model-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.list-footer {
|
||||
text-align: right;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.jeecg-basic-table-form-container {
|
||||
padding: 0;
|
||||
:deep(.ant-form) {
|
||||
background-color: transparent;
|
||||
}
|
||||
.table-page-search-submitButtons {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.add-knowledge-card {
|
||||
margin-bottom: 20px;
|
||||
background: #fcfcfd;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
height: 152px;
|
||||
width: calc(100% - 20px);
|
||||
.add-knowledge-card-icon {
|
||||
padding: 8px;
|
||||
color: #1f2329;
|
||||
background-color: #f5f6f7;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.add-knowledge-card-title {
|
||||
font-size: 16px;
|
||||
color:#1f2329;
|
||||
font-weight: 400;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.add-knowledge-card:hover {
|
||||
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
.model-icon{
|
||||
background-color: unset;
|
||||
border: none;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.model-icon:hover{
|
||||
color: #000000;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
border: none;
|
||||
}
|
||||
.ant-dropdown-link{
|
||||
font-size: 14px;
|
||||
height: 24px;
|
||||
padding: 0 7px;
|
||||
border-radius: 4px;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<BasicModal destroyOnClose @register="registerModal" :canFullscreen="false" width="600px" wrapClassName="ai-model-modal">
|
||||
<div class="modal">
|
||||
<div class="header">
|
||||
<span class="header-title">
|
||||
<span v-if="dataIndex ==='list' || dataIndex ==='add'" :class="dataIndex === 'list' ? '' : 'add-header-title pointer'" @click="goToList">选择供应商</span>
|
||||
<span v-if="dataIndex === 'add'" class="add-header-title"> > </span>
|
||||
<span v-if="dataIndex === 'add'" style="color: #1f2329">添加 {{ providerName }}</span>
|
||||
</span>
|
||||
|
||||
<a-select v-if="dataIndex === 'list'" :bordered="false" class="header-select" size="small" v-model:value="modelType" @change="handleChange">
|
||||
<a-select-option v-for="item in modelTypeOption" :value="item.value">{{ item.text }}</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="model-content" v-if="dataIndex === 'list'">
|
||||
<a-row :span="24">
|
||||
<a-col :xxl="12" :xl="12" :lg="12" :md="12" :sm="12" :xs="24" v-for="item in modelTypeList">
|
||||
<a-card class="model-card" @click="handleClick(item)">
|
||||
<div class="model-header">
|
||||
<div class="flex">
|
||||
<img :src="getImage(item.value)" class="header-img" />
|
||||
<div class="header-text">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<a-tabs v-model:activeKey="activeKey" v-if="dataIndex === 'add' || dataIndex === 'edit'">
|
||||
<a-tab-pane :key="1" tab="基础信息">
|
||||
<div class="model-content">
|
||||
<BasicForm @register="registerForm">
|
||||
<template #modelType="{ model, field }">
|
||||
<a-select v-model:value="model[field]" @change="handleModelTypeChange" :disabled="modelTypeDisabled">
|
||||
<a-select-option v-for="item in modelTypeAddOption" :value="item">
|
||||
<span v-if="item === 'LLM'">语言模型</span>
|
||||
<span v-else>向量模型</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<template #modelName="{ model, field }">
|
||||
<AutoComplete v-model:value="model[field]" :options="modelNameAddOption" :filter-option="filterOption">
|
||||
<template #option="{ value, label, descr, type }">
|
||||
<a-tooltip placement="right" color="#ffffff" :overlayInnerStyle="{ color:'#646a73' }">
|
||||
<template #title>
|
||||
<div v-html="getTitle(descr)"></div>
|
||||
</template>
|
||||
<div style="display: flex;justify-content: space-between;">
|
||||
<span>{{label}}</span>
|
||||
<div>
|
||||
<a-tag v-if="type && type.indexOf('text') != -1" color="#E8D7C3">文本</a-tag>
|
||||
<a-tag v-if="type && type.indexOf('image') != -1" color="#C3D9DC">图像分析</a-tag>
|
||||
<a-tag v-if="type && type.indexOf('vector') != -1" color="#D4E0D8">向量</a-tag>
|
||||
<a-tag v-if="type && type.indexOf('embeddings') != -1" color="#FFEBD3">文本嵌入</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</AutoComplete>
|
||||
</template>
|
||||
</BasicForm>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane :key="2" tab="高级配置" v-if="modelParamsShow">
|
||||
<AiModelSeniorForm ref="modelParamsRef" :modelParams="modelParams"></AiModelSeniorForm>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
</div>
|
||||
<template v-if="dataIndex === 'add' || dataIndex === 'edit'" #footer>
|
||||
<a-button @click="cancel">关闭</a-button>
|
||||
<a-button @click="save" type="primary">保存</a-button>
|
||||
</template>
|
||||
<template v-else #footer> </template>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import BasicModal from '@/components/Modal/src/BasicModal.vue';
|
||||
import { useModal, useModalInner } from '@/components/Modal';
|
||||
import { initDictOptions } from '@/utils/dict';
|
||||
import model from './model.json';
|
||||
import { AutoComplete } from 'ant-design-vue';
|
||||
|
||||
import BasicForm from '@/components/Form/src/BasicForm.vue';
|
||||
import { useForm } from '@/components/Form';
|
||||
import { formSchema, imageList } from '../model.data';
|
||||
import { editModel, queryById, saveModel } from '../model.api';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import AiModelSeniorForm from './AiModelSeniorForm.vue';
|
||||
export default {
|
||||
name: 'AddModelModal',
|
||||
components: {
|
||||
BasicForm,
|
||||
BasicModal,
|
||||
AiModelSeniorForm,
|
||||
AutoComplete,
|
||||
},
|
||||
emits: ['success', 'register'],
|
||||
setup(props, { emit }) {
|
||||
//ai类型数据
|
||||
const modelTypeData = ref<any>([]);
|
||||
//模型类型下拉框
|
||||
const modelTypeOption = ref<any>([]);
|
||||
//模型类型禁用状态
|
||||
const modelTypeDisabled = ref<boolean>(false);
|
||||
//模型类型
|
||||
const modelType = ref<string>('all');
|
||||
//模型供应商
|
||||
const modelTypeList = ref<any>([]);
|
||||
//list:供应商选择页面,add 添加编辑
|
||||
const dataIndex = ref<string>('list');
|
||||
//供应商名称
|
||||
const providerName = ref<string>('');
|
||||
//添加模型类型的option
|
||||
const modelTypeAddOption = ref<any>([]);
|
||||
//添加模型名称的option
|
||||
const modelNameAddOption = ref<any>([]);
|
||||
//模型数据
|
||||
const modelData = ref<any>({});
|
||||
//tab切换对应的key
|
||||
const activeKey = ref<number>(1);
|
||||
//模型参数
|
||||
const modelParams = ref<any>({});
|
||||
//是否显示模型参数
|
||||
const modelParamsShow = ref<boolean>(false);
|
||||
//模型参数ref
|
||||
const modelParamsRef = ref();
|
||||
|
||||
const getImage = (name) => {
|
||||
return imageList.value[name];
|
||||
};
|
||||
//自动填充文本搜索事件
|
||||
const filterOption = (input: string, option: any)=>{
|
||||
return option.value.toUpperCase().indexOf(input.toUpperCase()) >= 0;
|
||||
}
|
||||
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, validate, clearValidate }] = useForm({
|
||||
schemas: formSchema,
|
||||
showActionButtonGroup: false,
|
||||
layout: 'vertical',
|
||||
wrapperCol: { span: 24 },
|
||||
});
|
||||
|
||||
//注册modal
|
||||
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
|
||||
activeKey.value = 1;
|
||||
modelParamsShow.value = false;
|
||||
if(dataIndex.value !== 'list') {
|
||||
//重置表单
|
||||
await resetFields();
|
||||
}
|
||||
setModalProps({ minHeight: 500 });
|
||||
if (data.id) {
|
||||
dataIndex.value = 'edit';
|
||||
let values = await queryById({ id: data.id });
|
||||
if (values) {
|
||||
if(values.result.credential){
|
||||
let credential = JSON.parse(values.result.credential);
|
||||
if(credential.secretKey){
|
||||
values.result.secretKey = credential.secretKey;
|
||||
}
|
||||
if(credential.apiKey){
|
||||
values.result.apiKey = credential.apiKey;
|
||||
}
|
||||
}
|
||||
let provider = values.result.provider;
|
||||
let data = model.data.filter((item) => {
|
||||
return item.value.includes(provider);
|
||||
});
|
||||
if (data && data.length > 0) {
|
||||
modelTypeAddOption.value = data[0].type;
|
||||
modelNameAddOption.value = data[0][values.result.modelType];
|
||||
}
|
||||
if(values.result.modelType && values.result.modelType === 'LLM'){
|
||||
modelParamsShow.value = true;
|
||||
}
|
||||
if(values.result.modelParams){
|
||||
modelParams.value = JSON.parse(values.result.modelParams)
|
||||
}
|
||||
modelTypeDisabled.value = true;
|
||||
//表单赋值
|
||||
await setFieldsValue({
|
||||
...values.result,
|
||||
});
|
||||
//初始化模型提供者
|
||||
initModelProvider();
|
||||
}
|
||||
} else {
|
||||
modelTypeDisabled.value = false;
|
||||
//初始化模型提供者
|
||||
initModelProvider();
|
||||
dataIndex.value = 'list';
|
||||
modelNameAddOption.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
//初始化模型类型
|
||||
initModelTypeOption();
|
||||
|
||||
/**
|
||||
* 初始化 模型类型字典
|
||||
*/
|
||||
function initModelTypeOption() {
|
||||
initDictOptions('model_type').then((data) => {
|
||||
modelTypeOption.value = data;
|
||||
//update-begin---author:wangshuai---date:2025-03-04---for: 解决页面tab刷新一次就多一个全部类型的选项---
|
||||
if(data[0].value != 'all'){
|
||||
modelTypeOption.value.unshift({
|
||||
text: '全部类型',
|
||||
value: 'all',
|
||||
});
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-03-04---for: 解决页面tab刷新一次就多一个全部类型的选项---
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下拉框值选中事件
|
||||
* @param value
|
||||
*/
|
||||
function handleChange(value) {
|
||||
if ('all' == value) {
|
||||
modelTypeList.value = model.data;
|
||||
return;
|
||||
}
|
||||
let data = model.data.filter((item) => {
|
||||
return item.type.includes(value);
|
||||
});
|
||||
modelTypeList.value = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模型提供者
|
||||
*/
|
||||
function initModelProvider() {
|
||||
modelTypeList.value = model.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 供应商点击事件
|
||||
*
|
||||
* @param item
|
||||
*/
|
||||
function handleClick(item) {
|
||||
dataIndex.value = 'add';
|
||||
modelData.value = item;
|
||||
providerName.value = item.title;
|
||||
modelTypeAddOption.value = item.type;
|
||||
setTimeout(()=>{
|
||||
setFieldsValue({ 'provider': item.value, 'baseUrl': item.baseUrl })
|
||||
},100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
async function save() {
|
||||
try {
|
||||
setModalProps({ confirmLoading: true });
|
||||
let values = await validate();
|
||||
let credential = {
|
||||
apiKey: values.apiKey,
|
||||
secretKey: values.secretKey
|
||||
}
|
||||
if(modelParamsRef.value){
|
||||
let modelParams = modelParamsRef.value.emitChange();
|
||||
if(modelParams){
|
||||
values.modelParams = JSON.stringify(modelParams);
|
||||
}
|
||||
}
|
||||
values.credential = JSON.stringify(credential);
|
||||
//新增
|
||||
if (!values.id) {
|
||||
values.provider = modelData.value.value;
|
||||
await saveModel(values);
|
||||
closeModal();
|
||||
emit('success');
|
||||
} else {
|
||||
await editModel(values);
|
||||
closeModal();
|
||||
emit('success');
|
||||
}
|
||||
}catch(e){
|
||||
if(e.hasOwnProperty('errorFields')){
|
||||
activeKey.value = 1;
|
||||
}
|
||||
} finally {
|
||||
setModalProps({ confirmLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function cancel() {
|
||||
dataIndex.value = 'list';
|
||||
closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型类型选择事件
|
||||
* @param value
|
||||
*/
|
||||
async function handleModelTypeChange(value) {
|
||||
await setFieldsValue({ modelName: '' });
|
||||
await clearValidate('modelName');
|
||||
await setFieldsValue({
|
||||
modelName: modelData.value[value+'DefaultValue']
|
||||
})
|
||||
modelNameAddOption.value = modelData.value[value];
|
||||
if(value === 'LLM'){
|
||||
modelParamsShow.value = true;
|
||||
}else{
|
||||
modelParamsShow.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择供应商
|
||||
*/
|
||||
function goToList() {
|
||||
if (dataIndex.value === 'add') {
|
||||
dataIndex.value = 'list';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标题
|
||||
* @param title
|
||||
*/
|
||||
function getTitle(title) {
|
||||
if(!title){
|
||||
return "暂无描述内容";
|
||||
}
|
||||
return title.replaceAll("\n","<br>")
|
||||
}
|
||||
|
||||
return {
|
||||
registerModal,
|
||||
modelTypeData,
|
||||
modelTypeOption,
|
||||
modelType,
|
||||
handleChange,
|
||||
modelTypeList,
|
||||
getImage,
|
||||
handleClick,
|
||||
dataIndex,
|
||||
providerName,
|
||||
save,
|
||||
cancel,
|
||||
registerForm,
|
||||
handleModelTypeChange,
|
||||
modelTypeAddOption,
|
||||
modelNameAddOption,
|
||||
goToList,
|
||||
modelTypeDisabled,
|
||||
activeKey,
|
||||
modelParams,
|
||||
modelParamsShow,
|
||||
modelParamsRef,
|
||||
filterOption,
|
||||
getTitle,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.modal {
|
||||
padding: 12px 20px 20px 20px;
|
||||
.header {
|
||||
padding: 0 24px 24px 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.header-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.header-select {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.add-header-title {
|
||||
color: #646a73;
|
||||
}
|
||||
}
|
||||
.model-content {
|
||||
.model-header {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
.header-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.header-text {
|
||||
width: calc(100% - 80px);
|
||||
overflow: hidden;
|
||||
align-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.model-card {
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
:deep(.ant-card .ant-card-body) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.jeecg-basic-modal-close){
|
||||
span{
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.ai-model-modal{
|
||||
.jeecg-basic-modal-close > span{
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div class="model-params-popover">
|
||||
<div class="params" v-if="type === 'model'">
|
||||
<span style="font-size:14px">参数</span>
|
||||
<a-select value="加载预设" style="width: 96px" size="small" @change="onLoadPreset">
|
||||
<a-select-option v-for="(preset, idx) of presets" :value="idx" :key="idx">
|
||||
<a-space>
|
||||
<Icon :icon="preset.icon" />
|
||||
<span>{{ preset.name }}</span>
|
||||
</a-space>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<!-- 模型温度 -->
|
||||
<div class="setting-item" v-if="type === 'model'">
|
||||
<div class="label">
|
||||
<span>模型温度</span>
|
||||
<a-tooltip :title="tips.temperature">
|
||||
<Icon icon="ant-design:question-circle" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-switch v-model:checked="temperatureEnable" size="small"/>
|
||||
<a-slider v-bind="temperatureProps" v-model:value="model.temperature" :disabled="model['temperature'] === null"/>
|
||||
<a-input-number v-bind="temperatureProps" v-model:value="model.temperature" :disabled="model['temperature'] === null"/>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- 词汇属性 -->
|
||||
<div class="setting-item" v-if="type === 'model'">
|
||||
<div class="label">
|
||||
<span>词汇属性</span>
|
||||
<a-tooltip :title="tips.topP">
|
||||
<Icon icon="ant-design:question-circle" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-switch v-model:checked="topPEnable" size="small"/>
|
||||
<a-slider v-bind="topPProps" v-model:value="model.topP" :disabled="model['topP'] === null"/>
|
||||
<a-input-number v-bind="topPProps" v-model:value="model.topP" :disabled="model['topP'] === null"/>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- 话题属性 -->
|
||||
<div class="setting-item" v-if="type === 'model'">
|
||||
<div class="label">
|
||||
<span>话题属性</span>
|
||||
<a-tooltip :title="tips.presencePenalty">
|
||||
<Icon icon="ant-design:question-circle" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-switch v-model:checked="presencePenaltyEnable" size="small" />
|
||||
<a-slider v-bind="presencePenaltyProps" v-model:value="model.presencePenalty" :disabled="model['presencePenalty'] === null"/>
|
||||
<a-input-number v-bind="presencePenaltyProps" v-model:value="model.presencePenalty" :disabled="model['presencePenalty'] === null"/>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- 重复属性 -->
|
||||
<div class="setting-item" v-if="type === 'model'">
|
||||
<div class="label">
|
||||
<span>重复属性</span>
|
||||
<a-tooltip :title="tips.frequencyPenalty">
|
||||
<Icon icon="ant-design:question-circle" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-switch v-model:checked="frequencyPenaltyEnable" size="small" />
|
||||
<a-slider v-bind="frequencyPenaltyProps" v-model:value="model.frequencyPenalty" :disabled="model['frequencyPenalty'] === null"/>
|
||||
<a-input-number v-bind="frequencyPenaltyProps" v-model:value="model.frequencyPenalty" :disabled="model['frequencyPenalty'] === null"/>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- 最大回复 -->
|
||||
<div class="setting-item" v-if="type === 'model'">
|
||||
<div class="label">
|
||||
<span>最大回复</span>
|
||||
<a-tooltip :title="tips.maxTokens">
|
||||
<Icon icon="ant-design:question-circle" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-switch v-model:checked="maxTokensEnable" size="small" />
|
||||
<a-slider v-bind="maxTokensProps" v-model:value="model.maxTokens" :disabled="model['maxTokens'] === null"/>
|
||||
<a-input-number v-bind="maxTokensProps" v-model:value="model.maxTokens" :disabled="model['maxTokens'] === null"/>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- top k -->
|
||||
<div class="setting-item" v-if="type === 'knowledge'">
|
||||
<div class="label">
|
||||
<span>Top K</span>
|
||||
<a-tooltip :title="tips.topNumber">
|
||||
<Icon icon="ant-design:question-circle" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-switch v-model:checked="topNumberEnable" size="small" />
|
||||
<a-slider v-bind="topNumberProps" v-model:value="model.topNumber" :disabled="model['topNumber'] === null"/>
|
||||
<a-input-number v-bind="topNumberProps" v-model:value="model.topNumber" :disabled="model['topNumber'] === null"/>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- Score 阈值 -->
|
||||
<div class="setting-item" v-if="type === 'knowledge'">
|
||||
<div class="label">
|
||||
<span>Score 阈值</span>
|
||||
<a-tooltip :title="tips.similarity">
|
||||
<Icon icon="ant-design:question-circle" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-switch v-model:checked="similarityEnable" size="small" />
|
||||
<a-slider v-bind="similarityProps" v-model:value="model.similarity" :disabled="model['similarity'] === null"/>
|
||||
<a-input-number v-bind="similarityProps" v-model:value="model.similarity" :disabled="model['similarity'] === null"/>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { cloneDeep, omit } from 'lodash-es';
|
||||
|
||||
export default {
|
||||
name: 'AiModelSeniorForm',
|
||||
components: {},
|
||||
props: {
|
||||
modelParams: {
|
||||
type: Object,
|
||||
default: {}
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'model'
|
||||
}
|
||||
},
|
||||
emits: ['success', 'register', 'updateModel'],
|
||||
setup(props, { emit }) {
|
||||
// 预设参数
|
||||
const presets = [
|
||||
{
|
||||
name: '创意',
|
||||
icon: 'fxemoji:star',
|
||||
params: {
|
||||
temperature: 0.8,
|
||||
topP: 0.9,
|
||||
presencePenalty: 0.1,
|
||||
frequencyPenalty: 0.1,
|
||||
maxTokens: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '平衡',
|
||||
icon: 'noto:balance-scale',
|
||||
params: {
|
||||
temperature: 0.5,
|
||||
topP: 0.8,
|
||||
presencePenalty: 0.2,
|
||||
frequencyPenalty: 0.3,
|
||||
maxTokens: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '精确',
|
||||
icon: 'twemoji:direct-hit',
|
||||
params: {
|
||||
temperature: 0.2,
|
||||
topP: 0.7,
|
||||
presencePenalty: 0.5,
|
||||
frequencyPenalty: 0.5,
|
||||
maxTokens: null,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 参数介绍
|
||||
const tips = {
|
||||
temperature: '值越大,回复内容越赋有多样性创造性、随机性;设为0根据事实回答,希望得到精准答案应该降低该参数;日常聊天建议0.5-0.8。',
|
||||
topP: '值越小,Ai生成的内容越单调也越容易理解;值越大,Ai回复的词汇围越大,越多样化。',
|
||||
presencePenalty: '值越大,越能够让Ai更好地控制新话题的引入,建议微调或不变。',
|
||||
frequencyPenalty: '值越大,越能够让Ai更好地避免重复之前说过的话,建议微调或不变。',
|
||||
maxTokens:
|
||||
'设置Ai最大回复内容大小,会影响返回结果的长度。普通聊天建议500-800;短文生成建议800-2000;代码生成建议2000-3600;长文生成建议4000左右(或选择长回复模型)',
|
||||
topNumber: '用于筛选与用户问题相似度最高的文本片段。系统同时会根据选用模型上下文窗口大小动态调整分段数量。',
|
||||
similarity: '用于设置文本片段筛选的相似度阅值。'
|
||||
};
|
||||
|
||||
// 参数:温度
|
||||
const temperatureProps = ref<any>({
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
});
|
||||
|
||||
// 参数:词汇属性
|
||||
const topPProps = ref<any>({
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
});
|
||||
// 参数:话题属性
|
||||
const presencePenaltyProps = ref<any>({
|
||||
min: -2,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
});
|
||||
// 参数:重复属性
|
||||
const frequencyPenaltyProps = ref<any>({
|
||||
min: -2,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
});
|
||||
// 参数:最大回复
|
||||
const maxTokensProps = ref<any>({
|
||||
min: 1,
|
||||
max: 16000,
|
||||
step: 1,
|
||||
});
|
||||
|
||||
// 参数:topk
|
||||
const topNumberProps = ref<any>({
|
||||
min: 1,
|
||||
max: 10,
|
||||
step: 1,
|
||||
});
|
||||
|
||||
// 参数:Score 阈值
|
||||
const similarityProps = ref<any>({
|
||||
min: 0.1,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
});
|
||||
|
||||
|
||||
//参数对象
|
||||
const model = ref<any>(props.modelParams || {})
|
||||
|
||||
//模型温度是否勾选
|
||||
const temperatureEnable = computed({
|
||||
get:()=> model.value.temperature != null,
|
||||
set:(value) => model.value.temperature = !value? null: 0.7
|
||||
});
|
||||
|
||||
//词汇属性是否勾选
|
||||
const topPEnable = computed({
|
||||
get:()=> model.value.topP != null,
|
||||
set:(value) => model.value.topP = !value? null: 0
|
||||
});
|
||||
|
||||
//词汇属性是否勾选
|
||||
const presencePenaltyEnable = computed({
|
||||
get:()=> model.value.presencePenalty != null,
|
||||
set:(value) => model.value.presencePenalty = !value? null: 0
|
||||
});
|
||||
|
||||
//重复属性是否勾选
|
||||
const frequencyPenaltyEnable = computed({
|
||||
get:()=> model.value.frequencyPenalty != null,
|
||||
set:(value) => model.value.frequencyPenalty = !value? null: 0
|
||||
});
|
||||
|
||||
//最大回复
|
||||
const maxTokensEnable = computed({
|
||||
get:()=> model.value.maxTokens != null,
|
||||
set:(value) => model.value.maxTokens = !value? null: 520
|
||||
});
|
||||
|
||||
//top k
|
||||
const topNumberEnable = computed({
|
||||
get:()=> model.value.topNumber != null,
|
||||
set:(value) => model.value.topNumber = !value? null: 4
|
||||
});
|
||||
|
||||
//Score 阈值
|
||||
const similarityEnable = computed({
|
||||
get:()=> model.value.similarity != null,
|
||||
set:(value) => model.value.similarity = !value? null: 0.74
|
||||
});
|
||||
|
||||
// 加载预设
|
||||
function onLoadPreset(idx: number) {
|
||||
const preset = presets[idx];
|
||||
if (!preset) {
|
||||
return;
|
||||
}
|
||||
model.value = preset.params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新参数
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
function emitChange() {
|
||||
return model.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置modal值
|
||||
* @param values
|
||||
*/
|
||||
function setModalParams(values){
|
||||
model.value = values
|
||||
}
|
||||
|
||||
return {
|
||||
presets,
|
||||
onLoadPreset,
|
||||
tips,
|
||||
temperatureProps,
|
||||
topPProps,
|
||||
presencePenaltyProps,
|
||||
model,
|
||||
frequencyPenaltyProps,
|
||||
temperatureEnable,
|
||||
maxTokensProps,
|
||||
emitChange,
|
||||
topPEnable,
|
||||
presencePenaltyEnable,
|
||||
frequencyPenaltyEnable,
|
||||
maxTokensEnable,
|
||||
topNumberEnable,
|
||||
topNumberProps,
|
||||
similarityEnable,
|
||||
similarityProps,
|
||||
setModalParams,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.model-params-popover {
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
.params{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.setting-item{
|
||||
margin-top: 10px;
|
||||
}
|
||||
.setting-item .label {
|
||||
> span {
|
||||
vertical-align: middle;
|
||||
|
||||
&.app-iconify {
|
||||
cursor: help;
|
||||
color: #888888;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
.ant-slider {
|
||||
width: 380px;
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
width: 110px;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,148 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"title": "DeepSeek",
|
||||
"value": "DEEPSEEK",
|
||||
"LLM": [
|
||||
{"label": "deepseek-reasoner", "value": "deepseek-reasoner","descr": "【官方模型】深度求索 新推出的推理模型R1满血版\n火便全球。\n支持64k上下文,其中支持8k最大回复。","type": "text"},
|
||||
{"label":"deepseek-chat", "value": "deepseek-chat","descr": "最强开源 MoE 模型 DeepSeek-V3,全球首个在代码、数学能力上与GPT-4-Turbo争锋的模型,在代码、数学的多个榜单上位居全球第二;","type": "text"}
|
||||
],
|
||||
"type": ["LLM"],
|
||||
"baseUrl": "https://api.deepseek.com/v1",
|
||||
"LLMDefaultValue": "deepseek-chat"
|
||||
},
|
||||
{
|
||||
"title": "Ollama",
|
||||
"value": "OLLAMA",
|
||||
"LLM": [
|
||||
{"label": "llama2", "value": "llama2"},
|
||||
{"label": "llama2:13b", "value": "llama2:13b"},
|
||||
{"label": "llama2:70b", "value": "llama2:70b"},
|
||||
{"label": "llama2-chinese:13b", "value": "llama2-chinese:13b"},
|
||||
{"label": "llama3:8b", "value": "llama3:8b"},
|
||||
{"label": "llama3:70b", "value": "llama3:70b"},
|
||||
{"label": "qwen:0.5b", "value": "qwen:0.5b"},
|
||||
{"label": "qwen:1.8b", "value": "qwen:1.8b"},
|
||||
{"label": "qwen:4b", "value": "qwen:4b"},
|
||||
{"label": "qwen:7b", "value": "qwen:7b"},
|
||||
{"label": "qwen:14b", "value": "qwen:14b"},
|
||||
{"label": "qwen:32b", "value": "qwen:32b"},
|
||||
{"label": "qwen:72b", "value": "qwen:72b"},
|
||||
{"label": "qwen:110b", "value": "qwen:110b"},
|
||||
{"label": "qwen2:72b-instruct", "value": "qwen2:72b-instruct"},
|
||||
{"label": "qwen2:57b-a14b-instruct", "value": "qwen2:57b-a14b-instruct"},
|
||||
{"label": "qwen2:7b-instruct", "value": "qwen2:7b-instruct"},
|
||||
{"label": "qwen2.5:72b-instruct", "value": "qwen2.5:72b-instruct"},
|
||||
{"label": "qwen2.5:32b-instruct", "value": "qwen2.5:32b-instruct"},
|
||||
{"label": "qwen2.5:14b-instruct", "value": "qwen2.5:14b-instruct"},
|
||||
{"label": "qwen2.5:7b-instruct", "value": "qwen2.5:7b-instruct"},
|
||||
{"label": "qwen2.5:1.5b-instruct", "value": "qwen2.5:1.5b-instruct"},
|
||||
{"label": "qwen2.5:0.5b-instruct", "value": "qwen2.5:0.5b-instruct"},
|
||||
{"label": "qwen2.5:3b-instruct", "value": "qwen2.5:3b-instruct"},
|
||||
{"label": "phi3", "value": "phi3"}
|
||||
],
|
||||
"EMBED": [
|
||||
{"label": "nomic-embed-text", "value": "nomic-embed-text"}
|
||||
],
|
||||
"type": ["LLM", "EMBED"],
|
||||
"baseUrl": "http://localhost:11434",
|
||||
"LLMDefaultValue": "llama2",
|
||||
"EMBEDDefaultValue": "nomic-embed-text"
|
||||
},
|
||||
{
|
||||
"title": "OpenAI",
|
||||
"value": "OPENAI",
|
||||
"LLM": [
|
||||
{"label": "gpt-3.5-turbo", "value": "gpt-3.5-turbo","descr": "纯官方高速GPT3.5系列,目前指向gpt-35-turbo-0125模型,最大回复小于4k。\n综合能力强,过去使用最广泛的文本模型。", "type": "text"
|
||||
},
|
||||
{"label": "gpt-4", "value": "gpt-4","descr": "纯官方GPT4系列。知识库截止于2021年,价格适中,具有中等参数,比gpt-4turbo系列略强。","type": "text"},
|
||||
{"label": "gpt-4o", "value": "gpt-4o","descr": "GPT-4o,是openai的新旗舰型号,支持文本和图片分析。\n\n是迈向更自然的人机交互的一步——它接受文本和图像的任意组合作为输入,并生成文本和图像输出的任意组合。","type": "text,image"},
|
||||
{"label": "gpt-4o-mini", "value": "gpt-4o-mini","descr": "GPT-4o mini是目前性价比最高的小参数模型,性能介于GPT3.5~GPT4o之间。\n\n成本相比GPT-3.5 Turbo便宜60%以上,支持50种不同语言,用于替代GPT-3.5版本的模型。\n\n4o-mini的图像分析价格和4o差不多,如果有图像分析需求还是4o更好一些。\n\n当前指向 gpt-4o-mini-2024-07-18","type": "text,image"},
|
||||
{"label": "gpt-4-turbo", "value": "gpt-4-turbo","descr": "纯官方GPT4系列,支持文本和图片分析,最大回复4k,openai于2024-4-9新增的模型,知识库更新于2023年12月。提高了写作、数学、逻辑推理和编码能力。当前指向gpt-4-turbo-2024-04-09","type": "text,image"},
|
||||
{"label": "gpt-4-turbo-preview", "value": "gpt-4-turbo-preview","descr": "纯官方GPT4系列,最大回复4k,知识库更新于2023年4月。当前指向gpt-4-0125-preview","type": "text"},
|
||||
{"label": "gpt-3.5-turbo-0125", "value": "gpt-3.5-turbo-0125","descr": "openai于2024年1月25号更新的gpt-3.5模型,最大回复4k。\n\n综合能力强,过去使用最广泛的文本模型。","type": "text"},
|
||||
{"label": "gpt-3.5-turbo-1106", "value": "gpt-3.5-turbo-1106","descr": "openai于2023年11月6号更新的gpt-3.5模型,最大回复4k。属于即将被淘汰的模型。\n\n建议使用gpt-3.5-turbo或gpt-4o-mini","type": "text"},
|
||||
{"label": "gpt-3.5-turbo-0613", "value": "gpt-3.5-turbo-0613","descr": "通过微调后可以更准确地按照用户的指示进行操作,生成更简洁和针对性的输出。它不仅可以用于文本生成,还可以通过函数调用功能与其他系统和API进行集成,实现更复杂的任务自动化","type": "text"},
|
||||
{"label": "gpt-4o-2024-05-13", "value": "gpt-4o-2024-05-13","descr": "GPT-4o,是openai的新旗舰型号,支持文本和图片分析。\n\n是迈向更自然的人机交互的一步——它接受文本和图像的任意组合作为输入,并生成文本和图像输出的任意组合。\n\n该模型为初代的4o模型","type": "text,image"},
|
||||
{"label": "gpt-4-turbo-2024-04-09", "value": "gpt-4-turbo-2024-04-09","descr": "纯官方GPT4系列,支持文本和图片分析,最大回复4k,openai于2024-4-9新增的模型,提高了写作、数学、逻辑推理和编码能力。知识库更新于2023年12月。","type": "text,image"},
|
||||
{"label": "gpt-4-0125-preview", "value": "gpt-4-0125-preview","descr": "纯官方GPT4系列,最大回复4k,知识库更新于2023年4月。当前与gpt-4-turbo-preview属于同一模型","type": "text"},
|
||||
{"label": "gpt-4-1106-preview", "value": "gpt-4-1106-preview","descr": "纯官方GPT4系列,最大回复4k,知识库更新于2023年4月。正在逐渐被新的模型gpt-4-turbo和gpt-4-turbo-preview取代。","type": "text"}
|
||||
],
|
||||
"EMBED": [
|
||||
{"label": "text-embedding-ada-002", "value": "text-embedding-ada-002","descr": "用于生成文本嵌入的模型。文本嵌入是将文本转换为数值形式(通常是向量),以便可以用于机器学习模型。","type": "vector,embeddings"},
|
||||
{"label": "text-embedding-3-small", "value": "text-embedding-3-small","descr": "用于生成文本的嵌入表示,网络结构较小,计算资源需求较低。虽然可能不如\"large\"版本那样精准,但它更适合于资源受限的环境或需要更快速处理的任务。","type": "vector,embeddings"},
|
||||
{"label": "text-embedding-3-large", "value": "text-embedding-3-large","descr": "用于生成文本的嵌入表示,即将文本转换为高维空间中的点,这些点的距离可以表示文本之间的相似度。有较大的网络结构,能够捕捉更丰富的语言特征,适用于需要高质量文本相似度或分类任务的场景。","type": "vector,embeddings"}
|
||||
],
|
||||
"type": ["LLM", "EMBED"],
|
||||
"baseUrl": "https://api.openai.com/v1/",
|
||||
"LLMDefaultValue": "gpt-3.5-turbo",
|
||||
"EMBEDDefaultValue": "text-embedding-ada-002"
|
||||
},
|
||||
{
|
||||
"title": "通义千问",
|
||||
"value": "QWEN",
|
||||
"LLM": [
|
||||
{"label": "qwen-turbo", "value": "qwen-turbo","descr": "通义千问超大规模语言模型,支持中文、英文等不同语言输入。适合文本创作、文本处理、编程辅助、翻译服务、对话模拟。","type": "text"},
|
||||
{"label": "qwen-plus", "value": "qwen-plus","descr": "通义千问超大规模语言模型,支持中文、英文等不同语言输入。适合文本创作、文本处理、编程辅助、翻译服务、对话模拟。","type": "text"},
|
||||
{"label": "qwen-max", "value": "qwen-max","descr": "暂无描述内容!","type": "text"}
|
||||
],
|
||||
"EMBED": [
|
||||
{"label": "text-embedding-v2", "value": "text-embedding-v2","descr": "是一种将文本数据转换为向量的技术,通过深度学习模型将文本的语义信息嵌入到高维向量空间中。这些向量不仅能表达文本内容,还能捕捉文本之间的相似性和关系,从而让计算机高效地进行文本检索、分类、聚类等任务。","type": "vector"}
|
||||
],
|
||||
"type": ["LLM", "EMBED"],
|
||||
"baseUrl": "https://dashscope.aliyuncs.com/api/v1/services/",
|
||||
"LLMDefaultValue": "qwen-plus",
|
||||
"EMBEDDefaultValue": "text-embedding-v2"
|
||||
},
|
||||
{
|
||||
"title": "千帆大模型",
|
||||
"value": "QIANFAN",
|
||||
"LLM": [
|
||||
{"label": "ERNIE-Bot", "value": "ERNIE-Bot","descr": "是百度推出的一款知识增强大语言模型,主要用于与人对话互动、回答问题、协助创作,帮助人们高效便捷地获取信息、知识和灵感","type": "text"},
|
||||
{"label": "ERNIE-Bot 4.0", "value": "ERNIE-Bot 4.0","descr": "百度自行研发的文心产业级知识增强大语言模型4.0版本\n\n实现了基础模型的全面升级,在理解、生成、逻辑和记忆能力上相对ERNIE 3.5都有着显著提升,支持5K输入+2K输出。","type": "text"},
|
||||
{"label": "ERNIE-Bot-8K", "value": "ERNIE-Bot-8K","descr": "主要用于数据分析场景,特别是在企业数据分析中表现出色。ERNIE-Bot-8K是百度文心大模型的一个版本,具有模型效果优、生成能力强、应用门槛低等独特优势。","type": "text"},
|
||||
{"label": "ERNIE-Bot-turbo", "value": "ERNIE-Bot-turbo","descr": "是一个大语言模型,主要用于对话问答、内容创作生成等任务。它是百度自行研发的大语言模型,覆盖了海量中文数据,具有更强的对话问答和内容创作生成能力","type": "text"},
|
||||
{"label": "ERNIE-Speed-128K", "value": "ERNIE-Speed-128K","descr": "是一款基于Transformer结构的轻量级语言模型,旨在满足实时数据处理的需求。它具有高效、低延迟和高准确性的特点,广泛应用于自然语言处理、信息检索和文本分类等领域","type": "text"},
|
||||
{"label": "EB-turbo-AppBuilder", "value": "EB-turbo-AppBuilder","descr": "主要用于企业级应用场景,如智能客服、内容创作和知识问答等任务。它是基于文心高性能大语言模型ERNIE-Bot-turbo构建的,针对企业特定需求进行了深度的场景效果优化和输出格式定制,因此在满足企业特定需求方面具有更高的灵活性和实用性","type": "text"},
|
||||
{"label": "Yi-34B-Chat", "value": "Yi-34B-Chat","descr": "Yi-34B-Chat是一款基于Transformer架构的生成式预训练语言模型,它拥有340亿个参数,使其在处理自然语言任务时表现出了强大的能力。","type": "text"},
|
||||
{"label": "BLOOMZ-7B", "value": "BLOOMZ-7B","descr": "是一个用于生成文本序列的自回归模型,它可以进行多语言处理,支持46种语言和13种编程语言。BLOOMZ-7B是BLOOM模型的一个调优版本,具有更出色的泛化和零样本学习能力,适用于多种任务和场景","type": "text"},
|
||||
{"label": "Qianfan-BLOOMZ-7B-compressed", "value": "Qianfan-BLOOMZ-7B-compressed","descr": "是千帆团队在BLOOMZ-7B基础上的压缩版本,融合量化、稀疏化等技术,显存占用降低30%以上。","type": "text"},
|
||||
{"label": "Mixtral-8x7B-Instruct", "value": "Mixtral-8x7B-Instruct","descr": "由Mistral AI发布的首个高质量稀疏专家混合模型 (MOE),模型由8个70亿参数专家模型组成,在多个基准测试中表现优于Llama-2-70B及GPT3.5,能够处理32K上下文,在代码生成任务中表现尤为优异。","type": "text"},
|
||||
{"label": "Llama-2-7b-chat", "value": "Llama-2-7b-chat","descr": "由Meta AI研发并开源,在编码、推理及知识应用等场景表现优秀,Llama-2-7b-chat是高性能原生开源版本,适用于对话场景。","type": "text"},
|
||||
{"label": "Llama-2-13b-chat", "value": "Llama-2-13b-chat","descr": "由Meta AI研发并开源,在编码、推理及知识应用等场景表现优秀,Llama-2-13b-chat是性能与效果均衡的原生开源版本,适用于对话场景。","type": "text"},
|
||||
{"label": "Llama-2-70b-chat", "value": "Llama-2-70b-chat","descr": "由Meta AI研发并开源,在编码、推理及知识应用等场景表现优秀,Llama-2-70b-chat是高精度效果的原生开源版本。","type": "text"},
|
||||
{"label": "Qianfan-Chinese-Llama-2-7B", "value": "Qianfan-Chinese-Llama-2-7B","descr": "是千帆团队在Llama-2-7b基础上的中文增强版本,在CMMLU、C-EVAL等中文数据集上表现优异。","type": "text"},
|
||||
{"label": "ChatGLM2-6B-32K", "value": "ChatGLM2-6B-32K","descr": "是在ChatGLM2-6B的基础上进一步强化了对于长文本的理解能力,能够更好的处理最多32K长度的上下文。","type": "text"},
|
||||
{"label": "AquilaChat-7B", "value": "AquilaChat-7B","descr": "是由智源研究院研发,基于Aquila-7B训练的对话模型,支持流畅的文本对话及多种语言类生成任务,通过定义可扩展的特殊指令规范,实现 AquilaChat对其它模型和工具的调用,且易于扩展。","type": "text"}
|
||||
],
|
||||
"EMBED": [
|
||||
{"label": "Embedding-V1", "value": "Embedding-V1","descr": "主要用于将离散对象(如文本、图像等)映射为连续的数值向量,以便于计算机处理和机器学习模型的训练和使用","type": "vector,embeddings"},
|
||||
{"label": "tao-8k", "value": "tao-8k","descr": "是由Huggingface开发者amu研发并开源的长文本向量表示模型,支持8k上下文长度,模型效果在C-MTEB上居前列,是当前最优的中文长文本embeddings模型之一","type": "vector"},
|
||||
{"label": "bge-large-zh", "value": "bge-large-zh","descr": "是由智源研究院研发的中文版文本表示模型,可将任意文本映射为低维稠密向量,以用于检索、分类、聚类或语义匹配等任务,并可支持为大模型调用外部知识。","type": "vector"},
|
||||
{"label": "bge-large-en", "value": "bge-large-en","descr": "是由智源研究院研发的英文版文本表示模型,可将任意文本映射为低维稠密向量,以用于检索、分类、聚类或语义匹配等任务,并可支持为大模型调用外部知识。","type": "vector"}
|
||||
],
|
||||
"type": ["LLM", "EMBED"],
|
||||
"baseUrl": "https://aip.baidubce.com",
|
||||
"LLMDefaultValue": "Yi-34B-Chat",
|
||||
"EMBEDDefaultValue": "Embedding-V1"
|
||||
},
|
||||
{
|
||||
"title": "智谱AI",
|
||||
"value": "ZHIPU",
|
||||
"LLM": [
|
||||
{"label": "glm-4", "value": "glm-4","descr": "是一个多模态大语言模型,主要用于处理复杂的指令和任务,支持长文本处理、多模态理解和文生图等功能","type": "text,image"},
|
||||
{"label": "glm-4v", "value": "glm-4v","descr": "智谱:多模态模型\n\n更懂中文的视觉理解、文生图等多模态模型能力。准确理解各任务场景语言描述及指令,更精确的完成多模态理解类任务,或生成高质量的图片、视频等多模态内容。","type": "text,image"},
|
||||
{"label": "glm-4-flash", "value": "glm-4-flash","descr": "该模型官方免费,主要用于处理多种自然语言处理任务,包括智能对话助手、辅助论文翻译、ppt及会议内容生产、网页智能搜索、数据生成和抽取、网页解析、智能规划和决策、辅助科研等场景","type": "text"},
|
||||
{"label": "glm-3-turbo", "value": "glm-3-turbo","descr": "是一种基于transformer结构的语言模型,由智谱AI推出。其主要特点包括使用三层transformer结构、采用Turbo机制以实时生成文本、处理长文本输入并具有强大的语言理解能力","type": "text"}
|
||||
],
|
||||
"EMBED": [
|
||||
{"label": "Embedding-3", "value": "Embedding-3","descr": "主要用于文本搜索、聚类、推荐等任务。它通过将文本映射到低维向量空间,使得文本之间的语义关系可以通过向量之间的距离或相似度来衡量,从而支持各种基于向量的应用。","type": "vector"},
|
||||
{"label": "Embedding-2", "value": "Embedding-2","descr": "用于将高维离散数据映射到低维连续数值向量中,以便机器学习模型能够更好地处理和理解这些数据","type": "vector"}
|
||||
],
|
||||
"type": ["LLM", "EMBED"],
|
||||
"baseUrl": "https://open.bigmodel.cn",
|
||||
"LLMDefaultValue": "glm-4-flash",
|
||||
"EMBEDDefaultValue": "Embedding-2"
|
||||
}
|
||||
]
|
||||
}
|
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/OpenAi.png
Normal file
After Width: | Height: | Size: 867 B |
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/deepspeek.png
Normal file
After Width: | Height: | Size: 923 B |
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/ollama.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/qianfan.png
Normal file
After Width: | Height: | Size: 910 B |
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/qianwen.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
jeecgboot-vue3/src/views/super/airag/aimodel/icon/zhipuai.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
61
jeecgboot-vue3/src/views/super/airag/aimodel/model.api.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
enum Api {
|
||||
list = '/airag/airagModel/list',
|
||||
save = '/airag/airagModel/add',
|
||||
delete = '/airag/airagModel/delete',
|
||||
queryById = '/airag/airagModel/queryById',
|
||||
edit = '/airag/airagModel/edit',
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询AI模型
|
||||
* @param params
|
||||
*/
|
||||
export const list = (params) => {
|
||||
return defHttp.get({ url: Api.list, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据id查询AI模型
|
||||
* @param params
|
||||
*/
|
||||
export const queryById = (params) => {
|
||||
return defHttp.get({ url: Api.queryById, params }, { isTransformResponse: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增AI模型
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
export const saveModel = (params) => {
|
||||
return defHttp.post({ url: Api.save, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑AI模型
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
export const editModel = (params) => {
|
||||
return defHttp.put({ url: Api.edit, params });
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除数据权限
|
||||
*/
|
||||
export const deleteModel = (params, handleSuccess) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '是否删除名称为'+params.name+'的模型吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
return defHttp.delete({ url: Api.delete, params }, { joinParamsToUrl: true }).then(() => {
|
||||
handleSuccess();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
92
jeecgboot-vue3/src/views/super/airag/aimodel/model.data.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { FormSchema } from '@/components/Form';
|
||||
|
||||
import deepspeek from '/@/views/super/airag/aimodel/icon/deepspeek.png';
|
||||
import ollama from '/@/views/super/airag/aimodel/icon/ollama.png';
|
||||
import OpenAi from '/@/views/super/airag/aimodel/icon/OpenAi.png';
|
||||
import qianfan from '/@/views/super/airag/aimodel/icon/qianfan.png';
|
||||
import qianwen from '/@/views/super/airag/aimodel/icon/qianwen.png';
|
||||
import zhipuai from '/@/views/super/airag/aimodel/icon/zhipuai.png';
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 表单
|
||||
*/
|
||||
export const formSchema: FormSchema[] = [
|
||||
{
|
||||
label: 'id',
|
||||
field: 'id',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
label: '模型名称',
|
||||
field: 'name',
|
||||
required: true,
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
label: '模型类型',
|
||||
field: 'modelType',
|
||||
slot: 'modelType',
|
||||
required: true,
|
||||
component: 'Select',
|
||||
},
|
||||
{
|
||||
label: '基础模型',
|
||||
field: 'modelName',
|
||||
required: true,
|
||||
slot: 'modelName',
|
||||
component: 'Select',
|
||||
},
|
||||
{
|
||||
label: 'API域名',
|
||||
field: 'baseUrl',
|
||||
required: true,
|
||||
component: 'Input'
|
||||
},
|
||||
{
|
||||
label: 'API Key',
|
||||
field: 'apiKey',
|
||||
required: true,
|
||||
component: 'InputPassword',
|
||||
ifShow: ({ values }) => {
|
||||
if(values.provider==="OLLAMA"){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Secret Key',
|
||||
field: 'secretKey',
|
||||
required: true,
|
||||
component: 'InputPassword',
|
||||
ifShow: ({ values }) => {
|
||||
if(values.provider==='DEEPSEEK' || values.provider==="OLLAMA" || values.provider==="OPENAI"
|
||||
|| values.provider==="ZHIPU" || values.provider==="QWEN"){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '供应者',
|
||||
field: 'provider',
|
||||
component: 'Input',
|
||||
show: false,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 图片路径映射
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
export const imageList = ref<any>({
|
||||
DEEPSEEK: deepspeek,
|
||||
OLLAMA: ollama,
|
||||
OPENAI: OpenAi,
|
||||
QIANFAN: qianfan,
|
||||
QWEN: qianwen,
|
||||
ZHIPU: zhipuai,
|
||||
});
|