作者 lixiang

问题库以及自定义问答

import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
enum Api {
list = '/question/embedding/list',
save = '/question/embedding/add',
edit = '/question/embedding/edit',
deleteOne = '/question/embedding/delete',
deleteBatch = '/question/embedding/deleteBatch',
importExcel = '/question/embedding/importZip',
uploadZip = '/question/embedding/uploadZip',
}
export const getExportUrl = Api.importExcel;
export const getImportZipUrl = Api.uploadZip;
export const list = async (params) => {
try {
const res = await defHttp.get({
url: Api.list,
params: { ...params, size: 1000 }
});
if (res?.records && Array.isArray(res.records)) {
res.records = res.records.map(item => ({
...item,
...item.metadata,
question: item.question || '',
answer: item.answer || ''
}));
}
return res;
} catch (error) {
console.error("Error fetching question embeddings:", error);
return {
records: [],
total: 0,
size: 10,
current: 1,
pages: 0
};
}
}
export const deleteOne = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中问答数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
export const saveOrUpdate = (params, isUpdate) => {
const url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
... ...
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: 'ID',
align: 'center',
dataIndex: 'id',
width: 200,
},
{
title: '问题',
align: 'center',
dataIndex: 'question',
width: 250
},
{
title: '回答',
align: 'center',
dataIndex: 'answer',
width: 300
},
{
title: '原文',
align: 'center',
dataIndex: 'text',
width: 300
},
{
title: '元数据',
align: 'center',
dataIndex: 'metadata',
width: 200,
customRender: ({ text }) => JSON.stringify(text || {})
}
];
export const searchFormSchema: FormSchema[] = [
{
field: 'question',
label: '问题',
component: 'Input',
colProps: { span: 8 }
},
{
field: 'answer',
label: '回答',
component: 'Input',
colProps: { span: 8 }
}
];
export const formSchema: FormSchema[] = [
{
field: 'id',
label: 'ID',
component: 'Input',
show: false
},
{
field: 'question',
label: '问题',
component: 'InputTextArea',
required: true,
colProps: { span: 24 }
},
{
field: 'answer',
label: '回答',
component: 'InputTextArea',
required: true,
colProps: { span: 24 }
},
{
field: 'text',
label: '原文',
component: 'InputTextArea',
colProps: { span: 24 }
},
{
field: 'metadata',
label: '元数据',
component: 'InputTextArea',
colProps: { span: 24 }
}
];
export const superQuerySchema = {
question: { title: '问题', order: 0, view: 'text', type: 'string' },
answer: { title: '回答', order: 1, view: 'text', type: 'string' }
};
export function getBpmFormSchema(_formData): FormSchema[] {
return formSchema;
}
... ...
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined">新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined" />删除
</a-menu-item>
</a-menu>
</template>
<a-button>批量操作<Icon icon="mdi:chevron-down" /></a-button>
</a-dropdown>
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<QuestionEmbeddingModal @register="registerModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import QuestionEmbeddingModal from './components/QuestionEmbeddingModal.vue';
import { columns, searchFormSchema, superQuerySchema } from './QuestionEmbedding.data';
import { list, deleteOne, batchDelete, getImportZipUrl, getExportUrl } from './QuestionEmbedding.api';
const queryParam = reactive<any>({});
const [registerModal, { openModal }] = useModal();
const { tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '问答向量库',
api: list,
columns,
formConfig: {
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true
},
actionColumn: {
width: 120,
fixed: 'right'
},
beforeFetch: (params) => Object.assign(params, queryParam)
},
exportConfig: {
name: '问答向量库',
url: getExportUrl
},
importConfig: {
url: getImportZipUrl,
success: handleSuccess
}
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
const superQueryConfig = reactive(superQuerySchema);
function handleSuperQuery(params) {
Object.assign(queryParam, params);
reload();
}
function handleAdd() {
openModal(true, { isUpdate: false, showFooter: true });
}
function handleEdit(record) {
openModal(true, { record, isUpdate: true, showFooter: true });
}
function handleDetail(record) {
openModal(true, { record, isUpdate: true, showFooter: false });
}
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
function handleSuccess() {
selectedRowKeys.value = [];
reload();
}
function getTableAction(record) {
return [
{ label: '编辑', onClick: handleEdit.bind(null, record) }
];
}
function getDropDownAction(record) {
return [
{ label: '详情', onClick: handleDetail.bind(null, record) },
{
label: '删除',
popConfirm: {
title: '确认删除此问答?',
confirm: handleDelete.bind(null, record)
}
}
];
}
</script>
... ...
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" name="QuestionEmbeddingForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from '../QuestionEmbedding.data';
import { saveOrUpdate } from '../QuestionEmbedding.api';
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const isDetail = ref(false);
const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 }
});
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
setModalProps({
confirmLoading: false,
showCancelBtn: !!data?.showFooter,
showOkBtn: !!data?.showFooter
});
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
await setFieldsValue({
...data.record,
});
}
setProps({ disabled: !data?.showFooter });
});
const title = computed(() => (!unref(isUpdate) ? '新增问答' : !unref(isDetail) ? '问答详情' : '编辑问答'));
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
await saveOrUpdate(values, isUpdate.value);
closeModal();
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
... ...
... ... @@ -12,20 +12,53 @@ enum Api {
importExcel = '/embeddings/embeddings/importExcel',
exportXls = '/embeddings/embeddings/exportXls',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
export const list = async (params) => {
try {
// 设置足够大的size获取所有数据
const res = await defHttp.get({
url: Api.list,
params: { ...params, size: 1000 }
});
if (res?.records && Array.isArray(res.records)) {
// 将metadata中的字段提取到顶层
res.records = res.records.map(item => ({
...item,
...item.metadata, // 将metadata中的字段展开
docName: item.metadata?.docName || '',
knowledgeId: item.metadata?.knowledgeId || '',
docId: item.metadata?.docId || '',
index: item.metadata?.index || '',
}));
}
return res;
} catch (error) {
console.error("Error fetching data:", error);
return {
records: [],
total: 0,
size: 10,
current: 1,
pages: 0
};
}
}
/**
* 删除单个
... ... @@ -35,6 +68,7 @@ export const deleteOne = (params, handleSuccess) => {
handleSuccess();
});
};
/**
* 批量删除
* @param params
... ... @@ -53,6 +87,7 @@ export const batchDelete = (params, handleSuccess) => {
},
});
};
/**
* 保存或者更新
* @param params
... ...
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
//列表数据
// 列表数据
export const columns: BasicColumn[] = [
{
title: 'name',
title: 'ID',
align: 'center',
dataIndex: 'id',
width: 200,
},
{
title: '文本内容',
align: 'center',
dataIndex: 'name',
dataIndex: 'text',
width: 300
},
{
title: '文件名称',
align: 'center',
dataIndex: 'docName',
width: 150
},
{
title: '知识ID',
align: 'center',
dataIndex: 'knowledgeId',
width: 150
},
{
title: '文档ID',
align: 'center',
dataIndex: 'docId',
width: 150
},
{
title: '索引',
align: 'center',
dataIndex: 'index',
width: 80
},
{
title: '相似度',
align: 'center',
dataIndex: 'similarity',
width: 100
},
];
//查询数据
// 查询数据
export const searchFormSchema: FormSchema[] = [];
//表单数据
// 表单数据
export const formSchema: FormSchema[] = [
{
label: 'name',
field: 'name',
label: 'text',
field: 'text',
component: 'Input',
},
// TODO 主键隐藏字段,目前写死为ID
{
label: '',
field: 'id',
... ... @@ -28,7 +67,7 @@ export const formSchema: FormSchema[] = [
// 高级查询数据
export const superQuerySchema = {
name: { title: 'name', order: 0, view: 'text', type: 'string' },
name: { title: 'text', order: 0, view: 'text', type: 'string' },
};
/**
... ... @@ -36,6 +75,5 @@ export const superQuerySchema = {
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[] {
// 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
return formSchema;
}
... ...
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
enum Api {
send = '/airag/zdyRag/send',
}
/**
* 发送消息
* @param params
*/
export const sendMessage = async (params: { questionText: string }) => {
try {
const res = await defHttp.get({
url: Api.send,
params,
});
return res;
} catch (error) {
console.error("Error sending message:", error);
createMessage.error('发送消息失败');
return null;
}
};
// export const sendMessage = (params) => {
// return defHttp.get({ url: Api.send, params }, { isTransformResponse: false });
// };
// 保留其他API方法,但聊天界面可能不需要它们
export const list = async () => ({ records: [] });
export const deleteOne = async () => {};
export const batchDelete = async () => {};
export const getImportUrl = Api.send;
export const getExportUrl = Api.send;
... ...
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
// 列表数据
export const columns: BasicColumn[] = [
{
title: 'ID',
align: 'center',
dataIndex: 'id',
width: 200,
},
{
title: '文本内容',
align: 'center',
dataIndex: 'text',
width: 300
},
{
title: '文件名称',
align: 'center',
dataIndex: 'docName',
width: 150
},
{
title: '知识ID',
align: 'center',
dataIndex: 'knowledgeId',
width: 150
},
{
title: '文档ID',
align: 'center',
dataIndex: 'docId',
width: 150
},
{
title: '索引',
align: 'center',
dataIndex: 'index',
width: 80
},
{
title: '相似度',
align: 'center',
dataIndex: 'similarity',
width: 100
},
];
// 查询数据
export const searchFormSchema: FormSchema[] = [];
// 表单数据
export const formSchema: FormSchema[] = [
{
label: 'text',
field: 'text',
component: 'Input',
},
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
];
// 高级查询数据
export const superQuerySchema = {
name: { title: 'text', order: 0, view: 'text', type: 'string' },
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[] {
return formSchema;
}
... ...
<template>
<div class="chat-container">
<!-- 聊天消息区域 -->
<div class="chat-messages" ref="messagesContainer">
<div v-for="(message, index) in messages" :key="index" :class="['message', message.type]">
<div class="message-content">
<div class="message-text">{{ message.text }}</div>
<div v-if="message.type === 'assistant' && message.similarity" class="message-meta">
相似度: {{ (message.similarity * 100).toFixed(2) }}%
</div>
</div>
</div>
<div v-if="loading" class="message assistant">
<div class="message-content">
<div class="message-text">思考中...</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="chat-input">
<a-input
v-model:value="inputMessage"
placeholder="请输入您的问题..."
@pressEnter="sendMessage"
:disabled="loading"
/>
<a-button
type="primary"
@click="sendMessage"
:loading="loading"
:disabled="!inputMessage.trim()"
>
发送
</a-button>
</div>
</div>
</template>
<script lang="ts" name="zdy-rag-chat" setup>
import { ref, reactive, nextTick } from 'vue';
import { list, sendMessage as apiSendMessage } from './ZdyRag.api';
import { BasicTable, useTable } from '/@/components/Table';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
// 聊天消息数据
const messages = reactive([
{
type: 'assistant',
text: '您好!我是智能助手,请问有什么可以帮您?',
similarity: null
}
]);
const inputMessage = ref('');
const loading = ref(false);
const messagesContainer = ref<HTMLElement>();
// 发送消息
const sendMessage = async () => {
const text = inputMessage.value.trim();
if (!text || loading.value) return;
// 添加用户消息
messages.push({
type: 'user',
text: text,
similarity: null
});
inputMessage.value = '';
loading.value = true;
try {
// 调用API发送消息
const res = await apiSendMessage({ questionText: text });
console.log("res....",res)
if (res?.answer) {
messages.push({
type: 'assistant',
text: res.answer,
similarity: res.similarity || null
});
} else {
createMessage.error('获取回答失败');
}
} catch (error) {
console.error('发送消息失败:', error);
createMessage.error('发送消息失败');
} finally {
loading.value = false;
scrollToBottom();
}
};
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
});
};
</script>
<style lang="less" scoped>
.chat-container {
display: flex;
flex-direction: column;
height: calc(100vh - 120px);
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
border-radius: 8px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 10px;
margin-bottom: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.message {
margin-bottom: 15px;
display: flex;
&.user {
justify-content: flex-end;
.message-content {
background-color: #1890ff;
color: white;
border-radius: 18px 18px 0 18px;
}
}
&.assistant {
justify-content: flex-start;
.message-content {
background-color: #f0f0f0;
color: #333;
border-radius: 18px 18px 18px 0;
}
}
}
.message-content {
max-width: 70%;
padding: 10px 15px;
word-wrap: break-word;
}
.message-meta {
font-size: 12px;
color: #666;
margin-top: 5px;
text-align: right;
}
.chat-input {
display: flex;
gap: 10px;
:deep(.ant-input) {
flex: 1;
border-radius: 20px;
padding: 10px 15px;
}
.ant-btn {
border-radius: 20px;
padding: 0 20px;
}
}
</style>
... ...
<template>
<div style="min-height: 400px">
<BasicForm @register="registerForm"></BasicForm>
<div style="width: 100%;text-align: center" v-if="!formDisabled">
<a-button @click="submitForm" pre-icon="ant-design:check" type="primary">提 交</a-button>
</div>
</div>
</template>
<script lang="ts">
import {BasicForm, useForm} from '/@/components/Form/index';
import {computed, defineComponent} from 'vue';
import {defHttp} from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import {getBpmFormSchema} from '../ZdyRag.data';
import {saveOrUpdate} from '../ZdyRag.api';
export default defineComponent({
name: "TestForm",
components:{
BasicForm
},
props:{
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
},
setup(props){
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: {span: 24}
});
const formDisabled = computed(()=>{
if(props.formData.disabled === false){
return false;
}
return true;
});
let formData = {};
const queryByIdUrl = '/test/test/queryById';
async function initFormData(){
let params = {id: props.formData.dataId};
const data = await defHttp.get({url: queryByIdUrl, params});
formData = {...data}
//设置表单的值
await setFieldsValue(formData);
//默认是禁用
await setProps({disabled: formDisabled.value})
}
async function submitForm() {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
console.log('表单数据', params)
await saveOrUpdate(params, true)
}
initFormData();
return {
registerForm,
formDisabled,
submitForm,
}
}
});
</script>
... ...
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" name="TestForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import {ref, computed, unref} from 'vue';
import {BasicModal, useModalInner} from '/@/components/Modal';
import {BasicForm, useForm} from '/@/components/Form/index';
import {formSchema} from '../ZdyRag.data';
import {saveOrUpdate} from '../ZdyRag.api';
// Emits声明
const emit = defineEmits(['register','success']);
const isUpdate = ref(true);
const isDetail = ref(false);
//表单配置
const [registerForm, { setProps,resetFields, setFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: {span: 24}
});
//表单赋值
const [registerModal, {setModalProps, closeModal}] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({confirmLoading: false,showCancelBtn:!!data?.showFooter,showOkBtn:!!data?.showFooter});
isUpdate.value = !!data?.isUpdate;
isDetail.value = !!data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter })
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
setModalProps({confirmLoading: true});
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
} finally {
setModalProps({confirmLoading: false});
}
}
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
... ...