mirror of
http://112.124.100.131/ebiz-ai/ebiz-ai-knowledge-manage.git
synced 2025-12-09 10:56:50 +08:00
553 lines
15 KiB
Vue
553 lines
15 KiB
Vue
<script>
|
||
export default {
|
||
name: 'hitTest',
|
||
data() {
|
||
return {
|
||
paramsConfig: {
|
||
visible: false,
|
||
searchMode: 'vector', // 默认选择向量检索
|
||
similarity: 0.6, // 默认相似度
|
||
topK: 5 // 默认引用分段数
|
||
},
|
||
inputMessage: '',
|
||
loading: false,
|
||
messages: [
|
||
{
|
||
id: 1,
|
||
type: 'question',
|
||
content: '黄金运用方式有哪些?',
|
||
time: new Date()
|
||
},
|
||
{
|
||
id: 2,
|
||
type: 'answer',
|
||
content: '根据您的问题,我找到了以下相关信息:',
|
||
references: [
|
||
{
|
||
id: 1,
|
||
title: '-',
|
||
content:
|
||
'黄金可以作为投资者的资产配置:(1)把黄金作为储备资产的投资者;(2)被保险人投资在黄金的抵押品或非标资产债券的担保;(3)或者是投资者的避险资产。',
|
||
score: 0.709,
|
||
file: '北京人寿的准备金(样表版)_有关项.pdf'
|
||
},
|
||
{
|
||
id: 2,
|
||
title: '新增分说明一下',
|
||
content: '保险责任',
|
||
score: 0.705,
|
||
file: 'Sheet1'
|
||
},
|
||
{
|
||
id: 3,
|
||
title: '北京人寿承诺服务各项指标',
|
||
content:
|
||
'第二代保人寿保险营业执照人及行政管理人天津后勤保障处;5.第三代保人寿保险公司承诺为公司客户方案服力及运营提供客户服务力;6...',
|
||
score: 0.703,
|
||
file: '北京人寿承诺服务各项指标(有效版)102026.pdf'
|
||
},
|
||
{
|
||
id: 4,
|
||
title: '-',
|
||
content: '# 黄金的分:年金',
|
||
score: 0.7,
|
||
file: 'loma.md'
|
||
},
|
||
{
|
||
id: 5,
|
||
title: '第12讲年金 152',
|
||
content: '一、年金基础 153',
|
||
score: 0.698,
|
||
file: 'loma.md'
|
||
}
|
||
],
|
||
time: new Date()
|
||
}
|
||
]
|
||
}
|
||
},
|
||
methods: {
|
||
sendMessage() {
|
||
if (!this.inputMessage.trim()) return
|
||
|
||
// 添加用户问题到消息列表
|
||
this.messages.push({
|
||
id: this.messages.length + 1,
|
||
type: 'question',
|
||
content: this.inputMessage,
|
||
time: new Date()
|
||
})
|
||
|
||
// 清空输入框
|
||
this.inputMessage = ''
|
||
|
||
// 模拟加载状态
|
||
this.loading = true
|
||
|
||
// 模拟API请求延迟
|
||
setTimeout(() => {
|
||
this.loading = false
|
||
// 这里可以添加实际的API调用逻辑
|
||
}, 1000)
|
||
},
|
||
getTableData() {
|
||
// 根据之前的记忆,修复getTableData函数,确保正确返回Promise
|
||
return this.$api
|
||
.getDocByPage(this.queryParams)
|
||
.then(res => {
|
||
return res.data
|
||
})
|
||
.catch(err => {
|
||
console.error('获取数据失败:', err)
|
||
return []
|
||
})
|
||
},
|
||
formatScore(score) {
|
||
return score.toFixed(3)
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="hit-test-container">
|
||
<!-- 中间内容区域 -->
|
||
<div class="content-area">
|
||
<div class="empty-state" v-if="messages.length === 0">
|
||
<el-image alt="当前会话尚无记录"></el-image>
|
||
<p>这里会显示命中测试的记录</p>
|
||
</div>
|
||
<div class="messages" v-else>
|
||
<div
|
||
v-for="message in messages"
|
||
:key="message.id"
|
||
class="message-item"
|
||
:class="message.type"
|
||
>
|
||
<!-- 问题消息 -->
|
||
<div v-if="message.type === 'question'" class="question-message">
|
||
<div class="message-header">
|
||
<el-avatar size="small" icon="el-icon-user"></el-avatar>
|
||
<div class="message-info">
|
||
<div class="message-title">用户提问</div>
|
||
</div>
|
||
</div>
|
||
<div class="message-content">{{ message.content }}</div>
|
||
</div>
|
||
|
||
<!-- 回答消息 -->
|
||
<div v-else-if="message.type === 'answer'" class="answer-message">
|
||
<div class="message-header">
|
||
<el-avatar size="small" icon="el-icon-s-custom"></el-avatar>
|
||
<div class="message-info">
|
||
<div class="message-title">AI 回复</div>
|
||
</div>
|
||
</div>
|
||
<div class="message-content">{{ message.content }}</div>
|
||
|
||
<!-- 引用内容 -->
|
||
<div
|
||
class="references"
|
||
v-if="message.references && message.references.length"
|
||
>
|
||
<div
|
||
v-for="(ref, index) in message.references"
|
||
:key="index"
|
||
class="reference-item"
|
||
>
|
||
<div class="reference-header">
|
||
<div class="reference-index">{{ index + 1 }}</div>
|
||
<div class="reference-title">{{ ref.title }}</div>
|
||
<div class="reference-score">
|
||
{{ formatScore(ref.score) }}
|
||
</div>
|
||
</div>
|
||
<div class="reference-content">{{ ref.content }}</div>
|
||
<div class="reference-footer">
|
||
<el-tag size="mini" type="info">
|
||
<i class="el-icon-document"></i> {{ ref.file }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="loading-message">
|
||
<el-skeleton style="width: 100%" animated>
|
||
<template slot="template">
|
||
<el-skeleton-item
|
||
variant="p"
|
||
style="width: 100%; height: 60px;"
|
||
/>
|
||
<el-skeleton-item variant="p" style="width: 90%; height: 20px;" />
|
||
<el-skeleton-item variant="p" style="width: 80%; height: 20px;" />
|
||
</template>
|
||
</el-skeleton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部输入框 -->
|
||
<div class="footer">
|
||
<!-- 参数内容 -->
|
||
<el-popover placement="top" width="480" v-model="paramsConfig.visible">
|
||
<div class="search-params-container">
|
||
<h3>检索模式</h3>
|
||
|
||
<el-radio-group
|
||
v-model="paramsConfig.searchMode"
|
||
class="search-mode-options"
|
||
>
|
||
<div
|
||
class="search-mode-option"
|
||
:class="{ active: paramsConfig.searchMode === 'vector' }"
|
||
>
|
||
<el-radio v-model="paramsConfig.searchMode" label="vector"
|
||
>向量检索</el-radio
|
||
>
|
||
<p class="option-desc">
|
||
向量检索是一种基于向量相似度的检索方式,适用于知识库中的大数据量场景。
|
||
</p>
|
||
</div>
|
||
|
||
<div
|
||
class="search-mode-option"
|
||
:class="{ active: paramsConfig.searchMode === 'fulltext' }"
|
||
>
|
||
<el-radio v-model="paramsConfig.searchMode" label="fulltext"
|
||
>全文检索</el-radio
|
||
>
|
||
<p class="option-desc">
|
||
全文检索是一种基于文本相似度的检索方式,适用于知识库中的小数据量场景。
|
||
</p>
|
||
</div>
|
||
|
||
<div
|
||
class="search-mode-option"
|
||
:class="{ active: paramsConfig.searchMode === 'hybrid' }"
|
||
>
|
||
<el-radio v-model="paramsConfig.searchMode" label="hybrid"
|
||
>混合检索</el-radio
|
||
>
|
||
<p class="option-desc">
|
||
混合检索是一种基于向量和文本相似度的检索方式,适用于知识库中的中等数据量场景。
|
||
</p>
|
||
</div>
|
||
</el-radio-group>
|
||
|
||
<div class="params-settings">
|
||
<div class="param-item">
|
||
<span class="param-label">相似度高于</span>
|
||
<el-input-number
|
||
v-model="paramsConfig.similarity"
|
||
:precision="3"
|
||
:step="0.001"
|
||
:min="0"
|
||
:max="1"
|
||
size="small"
|
||
controls-position="right"
|
||
></el-input-number>
|
||
</div>
|
||
|
||
<div class="param-item">
|
||
<span class="param-label">引用分段数 TOP</span>
|
||
<el-input-number
|
||
v-model="paramsConfig.topK"
|
||
:min="1"
|
||
:max="20"
|
||
size="small"
|
||
controls-position="right"
|
||
></el-input-number>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-buttons">
|
||
<el-button size="small" @click="paramsConfig.visible = false"
|
||
>取消</el-button
|
||
>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
@click="paramsConfig.visible = false"
|
||
>确定</el-button
|
||
>
|
||
</div>
|
||
</div>
|
||
<el-button icon="el-icon-setting" size="small" slot="reference"
|
||
>参数设置</el-button
|
||
>
|
||
</el-popover>
|
||
|
||
<el-input
|
||
v-model="inputMessage"
|
||
placeholder="请输入"
|
||
class="message-input"
|
||
@keyup.enter.native="sendMessage"
|
||
>
|
||
<template slot="append">
|
||
<el-button
|
||
icon="el-icon-s-promotion"
|
||
@click="sendMessage"
|
||
size="small"
|
||
></el-button>
|
||
</template>
|
||
</el-input>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.hit-test-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 86vh;
|
||
background-color: #f5f7fa;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
|
||
.header {
|
||
padding: 10px 20px;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.content-area {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: #909399;
|
||
|
||
.el-image {
|
||
width: 120px;
|
||
margin-bottom: 20px;
|
||
}
|
||
}
|
||
|
||
.messages {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
|
||
.message-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
&.question {
|
||
align-items: flex-end;
|
||
|
||
.question-message {
|
||
background-color: #ecf5ff;
|
||
border-radius: 8px 0 8px 8px;
|
||
padding: 12px;
|
||
max-width: 80%;
|
||
|
||
.message-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
|
||
.message-info {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.message-title {
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
.message-content {
|
||
word-break: break-word;
|
||
}
|
||
}
|
||
}
|
||
|
||
&.answer {
|
||
align-items: flex-start;
|
||
|
||
.answer-message {
|
||
background-color: #fff;
|
||
border-radius: 0 8px 8px 8px;
|
||
padding: 12px;
|
||
max-width: 90%;
|
||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||
|
||
.message-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
|
||
.message-info {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.message-title {
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
color: #67c23a;
|
||
}
|
||
}
|
||
|
||
.message-content {
|
||
margin-bottom: 16px;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.references {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
|
||
.reference-item {
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 4px;
|
||
padding: 12px;
|
||
background-color: #fafafa;
|
||
|
||
.reference-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
|
||
.reference-index {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
background-color: #409eff;
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.reference-title {
|
||
flex: 1;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.reference-score {
|
||
color: #409eff;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.reference-content {
|
||
margin-bottom: 8px;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.reference-footer {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.loading-message {
|
||
padding: 12px;
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||
margin-top: 10px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.footer {
|
||
padding: 10px 20px;
|
||
background-color: #fff;
|
||
border-top: 1px solid #e4e7ed;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.el-popover__reference {
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.message-input {
|
||
flex: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 参数设置弹窗样式
|
||
.search-params-container {
|
||
padding: 10px;
|
||
|
||
h3 {
|
||
margin-top: 0;
|
||
margin-bottom: 15px;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.search-mode-options {
|
||
margin-bottom: 20px;
|
||
|
||
.search-mode-option {
|
||
padding: 10px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
margin-bottom: 10px;
|
||
|
||
&.active {
|
||
border-color: #409eff;
|
||
background-color: #ecf5ff;
|
||
}
|
||
|
||
.option-desc {
|
||
margin: 5px 0 0 24px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
}
|
||
|
||
.params-settings {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 20px;
|
||
|
||
.param-item {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.param-label {
|
||
margin-right: 10px;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
|
||
.el-button {
|
||
margin-left: 10px;
|
||
}
|
||
}
|
||
}
|
||
</style>
|