maxkb/ui/src/views/shared/hit-test-shared/index.vue
wangdan-fit2cloud 189564de21 feat: api
2025-06-16 19:02:08 +08:00

435 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="hit-test p-16-24">
<h4>
{{ $t('views.application.hitTest.title') }}
<el-text type="info" class="ml-4"> {{ $t('views.application.hitTest.text') }}</el-text>
</h4>
<el-card style="--el-card-padding: 0" class="hit-test__main p-16 mt-16 mb-16" v-loading="loading">
<div class="question-title" :style="{ visibility: questionTitle ? 'visible' : 'hidden' }">
<div class="avatar">
<el-avatar>
<img src="@/assets/user-icon.svg" style="width: 54%" alt="" />
</el-avatar>
</div>
<div class="content">
<h4 class="text break-all">{{ questionTitle }}</h4>
</div>
</div>
<el-scrollbar>
<div class="hit-test-height">
<el-empty
v-if="first"
:image="emptyImg"
:description="$t('views.application.hitTest.emptyMessage1')"
style="padding-top: 160px"
:image-size="125"
/>
<el-empty
v-else-if="paragraphDetail.length == 0"
:description="$t('views.application.hitTest.emptyMessage2')"
style="padding-top: 160px"
:image-size="125"
/>
<el-row v-else>
<el-col
:xs="24"
:sm="12"
:md="12"
:lg="8"
:xl="6"
v-for="(item, index) in paragraphDetail"
:key="index"
class="p-8"
>
<CardBox
shadow="hover"
:title="item.title || '-'"
:description="item.content"
class="document-card layout-bg layout-bg cursor"
:class="item.is_active ? '' : 'disabled'"
:showIcon="false"
@click="editParagraph(item)"
>
<template #icon>
<el-avatar class="mr-12 avatar-light" :size="22"> {{ index + 1 + '' }}</el-avatar>
</template>
<div class="active-button primary">{{ item.similarity?.toFixed(3) }}</div>
<template #footer>
<div class="footer-content flex-between">
<el-text>
<el-icon>
<Document />
</el-icon>
{{ item?.document_name }}
</el-text>
<div v-if="item.trample_num || item.star_num">
<span v-if="item.star_num">
<AppIcon iconName="app-like-color"></AppIcon>
{{ item.star_num }}
</span>
<span v-if="item.trample_num" class="ml-4">
<AppIcon iconName="app-oppose-color"></AppIcon>
{{ item.trample_num }}
</span>
</div>
</div>
</template>
</CardBox>
</el-col>
</el-row>
</div>
</el-scrollbar>
</el-card>
<ParagraphDialog ref="ParagraphDialogRef" :title="title" @refresh="refresh" />
<div class="hit-test__operate">
<el-popover :visible="popoverVisible" placement="right-end" :width="500" trigger="click">
<template #reference>
<el-button icon="Setting" class="mb-8" @click="settingChange('open')">{{
$t('common.paramSetting')
}}</el-button>
</template>
<div class="mb-16">
<div class="title mb-8">
{{ $t('views.application.dialog.selectSearchMode') }}
</div>
<el-radio-group
v-model="cloneForm.search_mode"
class="card__radio"
@change="changeHandle"
>
<el-card
shadow="never"
class="mb-16"
:class="cloneForm.search_mode === 'embedding' ? 'active' : ''"
>
<el-radio value="embedding" size="large">
<p class="mb-4">
{{ $t('views.application.dialog.vectorSearch') }}
</p>
<el-text type="info">{{
$t('views.application.dialog.vectorSearchTooltip')
}}</el-text>
</el-radio>
</el-card>
<el-card
shadow="never"
class="mb-16"
:class="cloneForm.search_mode === 'keywords' ? 'active' : ''"
>
<el-radio value="keywords" size="large">
<p class="mb-4">
{{ $t('views.application.dialog.fullTextSearch') }}
</p>
<el-text type="info">{{
$t('views.application.dialog.fullTextSearchTooltip')
}}</el-text>
</el-radio>
</el-card>
<el-card
shadow="never"
class="mb-16"
:class="cloneForm.search_mode === 'blend' ? 'active' : ''"
>
<el-radio value="blend" size="large">
<p class="mb-4">
{{ $t('views.application.dialog.hybridSearch') }}
</p>
<el-text type="info">{{
$t('views.application.dialog.hybridSearchTooltip')
}}</el-text>
</el-radio>
</el-card>
</el-radio-group>
</div>
<el-row :gutter="20">
<el-col :span="12">
<div class="mb-16">
<div class="title mb-8">
{{ $t('views.application.dialog.similarityThreshold') }}
</div>
<el-input-number
v-model="cloneForm.similarity"
:min="0"
:max="cloneForm.search_mode === 'blend' ? 2 : 1"
:precision="3"
:step="0.1"
:value-on-clear="0"
controls-position="right"
class="w-full"
/>
</div>
</el-col>
<el-col :span="12">
<div class="mb-16">
<div class="title mb-8">
{{ $t('views.application.dialog.topReferences') }}
</div>
<el-input-number
v-model="cloneForm.top_number"
:min="1"
:max="10000"
controls-position="right"
class="w-full"
/>
</div>
</el-col>
</el-row>
<div class="text-right">
<el-button @click="popoverVisible = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="settingChange('close')">{{
$t('common.confirm')
}}</el-button>
</div>
</el-popover>
<div class="operate-textarea flex">
<el-input
ref="quickInputRef"
v-model="inputValue"
type="textarea"
:placeholder="$t('common.inputPlaceholder')"
:autosize="{ minRows: 1, maxRows: 8 }"
@keydown.enter="sendChatHandle($event)"
/>
<div class="operate">
<el-button
text
class="sent-button"
:disabled="isDisabledChart || loading"
@click="sendChatHandle"
>
<img v-show="isDisabledChart || loading" src="@/assets/icon_send.svg" alt="" />
<img
v-show="!isDisabledChart && !loading"
src="@/assets/icon_send_colorful.svg"
alt=""
/>
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { cloneDeep } from 'lodash'
import KnowledgeApi from '@/api/shared/knowledge'
// import applicationApi from '@/api/application/application'
import ParagraphDialog from '@/views/paragraph/component/ParagraphDialog.vue'
import { arraySort } from '@/utils/common'
import emptyImg from '@/assets/hit-test-empty.png'
import { t } from '@/locales'
const route = useRoute()
const {
meta: { activeMenu },
params: { id },
} = route as any
const quickInputRef = ref()
const ParagraphDialogRef = ref()
const loading = ref(false)
const paragraphDetail = ref<any[]>([])
const title = ref('')
const inputValue = ref('')
const formInline = ref({
similarity: 0.6,
top_number: 5,
search_mode: 'embedding',
})
// 第一次加载
const first = ref(true)
const cloneForm = ref<any>({})
const popoverVisible = ref(false)
const questionTitle = ref('')
const isDisabledChart = computed(() => !inputValue.value)
const isApplication = computed(() => {
return activeMenu.includes('application')
})
const isDataset = computed(() => {
return activeMenu.includes('knowledge')
})
function changeHandle(val: string) {
if (val === 'keywords') {
cloneForm.value.similarity = 0
} else {
cloneForm.value.similarity = 0.6
}
}
function settingChange(val: string) {
if (val === 'open') {
popoverVisible.value = true
cloneForm.value = cloneDeep(formInline.value)
} else if (val === 'close') {
popoverVisible.value = false
formInline.value = cloneDeep(cloneForm.value)
}
}
function editParagraph(row: any) {
title.value = t('views.paragraph.paragraphDetail')
ParagraphDialogRef.value.open(row)
}
function sendChatHandle(event: any) {
if (!event?.ctrlKey && !event?.shiftKey && !event?.altKey && !event?.metaKey) {
// 如果没有按下组合键,则会阻止默认事件
event.preventDefault()
if (!isDisabledChart.value && !loading.value) {
getHitTestList()
}
} else {
// 如果同时按下ctrl/shift/cmd/opt +enter则会换行
insertNewlineAtCursor(event)
}
}
const insertNewlineAtCursor = (event?: any) => {
const textarea = quickInputRef.value.$el.querySelector(
'.el-textarea__inner',
) as HTMLTextAreaElement
const startPos = textarea.selectionStart
const endPos = textarea.selectionEnd
// 阻止默认行为(避免额外的换行符)
event.preventDefault()
// 在光标处插入换行符
inputValue.value = inputValue.value.slice(0, startPos) + '\n' + inputValue.value.slice(endPos)
nextTick(() => {
textarea.setSelectionRange(startPos + 1, startPos + 1) // 光标定位到换行后位置
})
}
function getHitTestList() {
const obj = {
query_text: inputValue.value,
...formInline.value,
}
if (isDataset.value) {
KnowledgeApi.getKnowledgeHitTest(id, obj, loading).then((res) => {
paragraphDetail.value = res.data && arraySort(res.data, 'comprehensive_score', true)
questionTitle.value = inputValue.value
inputValue.value = ''
first.value = false
})
} else if (isApplication.value) {
// applicationApi.getApplicationHitTest(id, obj, loading).then((res) => {
// paragraphDetail.value = res.data && arraySort(res.data, 'comprehensive_score', true)
// questionTitle.value = inputValue.value
// inputValue.value = ''
// first.value = false
// })
}
}
function refresh(data: any) {
if (data) {
const obj = paragraphDetail.value.filter((v) => v.id === data.id)[0]
obj.content = data.content
obj.title = data.title
} else {
paragraphDetail.value = []
getHitTestList()
}
}
onMounted(() => {})
</script>
<style lang="scss" scoped>
.hit-test {
.question-title {
.avatar {
float: left;
}
.content {
padding-left: 40px;
.text {
padding: 6px 0;
height: 34px;
box-sizing: border-box;
}
}
}
&__operate {
.operate-textarea {
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
background-color: #ffffff;
border-radius: 8px;
border: 1px solid #ffffff;
box-sizing: border-box;
&:has(.el-textarea__inner:focus) {
border: 1px solid var(--el-color-primary);
}
:deep(.el-textarea__inner) {
border-radius: 8px !important;
box-shadow: none;
resize: none;
padding: 12px 16px;
}
.operate {
padding: 6px 10px;
.sent-button {
max-height: none;
.el-icon {
font-size: 24px;
}
}
:deep(.el-loading-spinner) {
margin-top: -15px;
.circular {
width: 31px;
height: 31px;
}
}
}
}
}
}
.hit-test {
&__header {
position: absolute;
right: calc(var(--app-base-px) * 3);
}
.hit-test-height {
height: calc(100vh - 300px);
}
.document-card {
height: 210px;
border: 1px solid var(--app-layout-bg-color);
&:hover {
background: #ffffff;
border: 1px solid var(--el-border-color);
}
&.disabled {
background: var(--app-layout-bg-color);
border: 1px solid var(--app-layout-bg-color);
:deep(.description) {
color: var(--app-border-color-dark);
}
:deep(.title) {
color: var(--app-border-color-dark);
}
}
:deep(.description) {
-webkit-line-clamp: 5 !important;
height: 110px;
}
.active-button {
position: absolute;
right: 16px;
top: 16px;
}
}
}
</style>