AIGC应用平台+知识库模块

This commit is contained in:
JEECG 2025-04-07 14:08:27 +08:00
parent 45e9b03e2d
commit 507289ff6c
52 changed files with 9147 additions and 161 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View 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 });
};

View 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,
}
},
];

View 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>

View 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(() => {
// watchwatch
//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>

View File

@ -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';
//aiChatref
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>

View 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-11495AI---
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-11495AI---
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>

View 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>

View File

@ -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;
});

View File

@ -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,
}
};
}

View File

@ -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,
};
}

View 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;
})();

View File

@ -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,
};
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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);
// activeactive
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>

View File

@ -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 {

View File

@ -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;
}
}

View 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;
}
}
}

View File

@ -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":"啥是电影蒙太奇呀?"}]
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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-113248.head---
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
//
await setFieldsValue({
...data.record,
});
}
//update-end---author:wangshuai---date:2025-03-11---for:QQYUN-113248.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-113248.head---
if(isUpdate.value){
//
emit('success', values);
}else{
//
emit('success', result);
}
//update-end---author:wangshuai---date:2025-03-11---for: QQYUN-113248.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>

View File

@ -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>

View File

@ -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>

View File

@ -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-11649AI---
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-11649AI---
}
/**
* 复制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>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -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 });
};

View File

@ -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;
}
},
];

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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']);
//keytable
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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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>

View File

@ -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>({});
//tabkey
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>

View File

@ -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>

View File

@ -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系列支持文本和图片分析最大回复4kopenai于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系列支持文本和图片分析最大回复4kopenai于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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View 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();
});
},
});
};

View 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,
});