作者 lixiang

问题库以及自定义问答

... ... @@ -16,22 +16,22 @@ export const sendMessage = async (params: { questionText: string }) => {
const res = await defHttp.get({
url: Api.send,
params,
timeout: 60000
});
return res;
console.log("res...",res)
// 确保返回的数据结构正确
if (res ) {
return {
answer: res.answer || '',
similarity: res.similarity || 0,
fileName: res.fileName || '',
fileBase64: res.fileBase64 || null
};
}
return null;
} 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;
... ...
<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 class="chat-layout">
<!-- 聊天消息区域 -->
<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" v-html="message.text"></div>
<div v-if="message.type === 'assistant' && message.similarity" class="message-meta">
相似度: {{ (message.similarity * 100).toFixed(2) }}%
<a v-if="message.fileBase64" @click="showPreview(message)">预览文件</a>
</div>
</div>
</div>
<div v-if="loading" class="message assistant">
<div class="message-content">
<div class="message-text">思考中...</div>
</div>
</div>
</div>
<div v-if="loading" class="message assistant">
<div class="message-content">
<div class="message-text">思考中...</div>
<!-- 文件预览区域 - 右侧 -->
<div v-if="previewContent" class="file-preview">
<div class="preview-header">
<span>文件预览</span>
<a-button type="text" @click="closePreview">
<template #icon><CloseOutlined /></template>
</a-button>
</div>
<div class="preview-content">
<div v-if="previewType === 'txt' || previewType === 'md'" class="text-preview">
<pre>{{ previewContent }}</pre>
</div>
<div v-else-if="previewType === 'doc' || previewType === 'docx'" class="doc-preview">
<!-- 使用Blob URL直接预览 -->
<iframe
:src="previewUrl"
frameborder="0"
style="width:100%; height:100%;"
></iframe>
</div>
</div>
</div>
</div>
... ... @@ -39,9 +65,9 @@
<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 { sendMessage as apiSendMessage } from './ZdyRag.api';
import { useMessage } from '/@/hooks/web/useMessage';
import { CloseOutlined } from '@ant-design/icons-vue';
const { createMessage } = useMessage();
... ... @@ -50,13 +76,17 @@ const messages = reactive([
{
type: 'assistant',
text: '您好!我是智能助手,请问有什么可以帮您?',
similarity: null
similarity: null,
fileBase64: null
}
]);
const inputMessage = ref('');
const loading = ref(false);
const messagesContainer = ref<HTMLElement>();
const previewContent = ref('');
const previewType = ref('');
const previewUrl = ref('');
// 发送消息
const sendMessage = async () => {
... ... @@ -67,34 +97,102 @@ const sendMessage = async () => {
messages.push({
type: 'user',
text: text,
similarity: null
similarity: null,
fileBase64: null
});
inputMessage.value = '';
loading.value = true;
closePreview();
try {
// 调用API发送消息
const res = await apiSendMessage({ questionText: text });
console.log("res....",res)
if (res?.answer) {
messages.push({
console.log('API响应:', res);
if (res) {
const newMessage = {
type: 'assistant',
text: res.answer,
similarity: res.similarity || null
});
text: formatAnswer(res.answer),
similarity: res.similarity || null,
fileBase64: res.fileBase64 || null,
fileName: res.fileName || ''
};
messages.push(newMessage);
// 如果有文件内容,自动显示预览
if (res.fileBase64 && res.fileName) {
showPreview(newMessage);
}
} else {
createMessage.error('获取回答失败');
createMessage.error('获取回答失败: 返回数据为空');
}
} catch (error) {
console.error('发送消息失败:', error);
createMessage.error('发送消息失败');
createMessage.error('发送消息失败: ' + (error as Error).message);
} finally {
loading.value = false;
scrollToBottom();
}
};
// 显示文件预览
const showPreview = (message) => {
if (!message.fileBase64 || !message.fileName) {
console.warn('无法预览: 缺少fileBase64或fileName');
return;
}
try {
const fileExt = message.fileName.split('.').pop()?.toLowerCase() || '';
previewType.value = fileExt;
if (fileExt === 'txt' || fileExt === 'md') {
// 对于文本文件,直接解码显示
previewContent.value = atob(message.fileBase64);
} else if (fileExt === 'doc' || fileExt === 'docx') {
// 对于Word文档,创建Blob对象并生成URL
const byteCharacters = atob(message.fileBase64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], {
type: fileExt === 'docx' ?
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' :
'application/msword'
});
// 直接使用Blob URL
previewUrl.value = URL.createObjectURL(blob);
previewContent.value = 'doc-preview';
} else {
console.warn('不支持的文件类型:', fileExt);
createMessage.warning(`不支持预览 ${fileExt} 格式的文件`);
}
} catch (error) {
console.error('文件预览失败:', error);
createMessage.error('文件预览失败');
}
};
// 关闭预览
const closePreview = () => {
previewContent.value = '';
previewType.value = '';
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
previewUrl.value = '';
}
};
// 格式化回答内容
const formatAnswer = (answer: string) => {
// 替换[n]为换行符,然后将换行符转换为<br>标签
return answer.replace(/\[n\]/g, '\n').replace(/\n/g, '<br>');
};
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
... ... @@ -110,23 +208,71 @@ const scrollToBottom = () => {
display: flex;
flex-direction: column;
height: calc(100vh - 120px);
max-width: 800px;
max-width: 1200px; /* 增加宽度以容纳两个区域 */
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
border-radius: 8px;
}
.chat-messages {
.chat-layout {
display: flex;
flex: 1;
gap: 20px;
overflow: hidden;
margin-bottom: 20px;
}
.chat-messages {
flex: 2;
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);
}
.file-preview {
flex: 1;
min-width: 350px; /* 确保预览区域有最小宽度 */
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #f0f0f0;
border-bottom: 1px solid #e8e8e8;
}
.preview-content {
flex: 1;
overflow: auto;
.text-preview {
padding: 15px;
white-space: pre-wrap;
font-family: monospace;
}
.doc-preview {
height: 100%;
iframe {
width: 100%;
height: 100%;
border: none;
}
}
}
}
.message {
margin-bottom: 15px;
display: flex;
... ... @@ -163,6 +309,16 @@ const scrollToBottom = () => {
color: #666;
margin-top: 5px;
text-align: right;
a {
margin-left: 10px;
color: #1890ff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.chat-input {
... ...
<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>
<!--<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}
});
<!--<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;
});
<!-- 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})
}
<!-- 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)
}
<!-- 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>
<!-- 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>
<!--<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>
<!--<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%;
}
<!--<style lang="less" scoped>-->
<!-- /** 时间和数字输入框样式 */-->
<!-- :deep(.ant-input-number) {-->
<!-- width: 100%;-->
<!-- }-->
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
<!-- :deep(.ant-calendar-picker) {-->
<!-- width: 100%;-->
<!-- }-->
<!--</style>-->
... ...