index.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <template>
  2. <div class="bm-main-box">
  3. <div class="bm-main-box-title">
  4. 召回测试
  5. <span>根据给定的查询文本测试知识的召回效果</span>
  6. </div>
  7. <div class="mt-[var(--czr-gap)] flex flex-1 overflow-hidden">
  8. <div class="card rounded-bl-2 flex flex-1 flex-col gap-4">
  9. <div class="text-main" v-loading="state.loading">
  10. <div class="text-head">
  11. 源文本
  12. <div class="__hover" @click="onModelConfig">
  13. {{ SearchMethodTypeMap.get(state.modelConfig.indexConfig?.type) }}
  14. </div>
  15. </div>
  16. <div class="text-content">
  17. <CzrFormColumn
  18. label-width="0px"
  19. width="100%"
  20. v-model:param="state.text"
  21. type="textarea"
  22. :transparent="true"
  23. :clearable="false"
  24. />
  25. <CzrButton
  26. class="test"
  27. type="primary"
  28. title="测试"
  29. @click="onTest"
  30. />
  31. </div>
  32. </div>
  33. <div class="flex flex-1 flex-col">
  34. <div class="__czr-title_1">记录</div>
  35. <div class="flex-1">
  36. <CzrTable
  37. v-loading="state.query.loading"
  38. :data="state.query.result.data"
  39. :head="state.query.head"
  40. :total="state.query.result.total"
  41. :page="state.query.page.pageNum"
  42. :pageSize="state.query.page.pageSize"
  43. @handlePage="onPage"
  44. @row-click="(row) => (state.text = row.content)"
  45. >
  46. </CzrTable>
  47. </div>
  48. </div>
  49. </div>
  50. <div
  51. class="card rounded-br-2 flex w-[26.25rem] flex-col space-y-4"
  52. v-loading="state.loading"
  53. >
  54. <div class="__czr-title_1">{{ state.backList.length }} 个召回段落</div>
  55. <div class="flex flex-1 flex-col gap-4 overflow-y-auto">
  56. <template v-for="(item, index) in state.backList">
  57. <div
  58. class="back-item"
  59. :style="{
  60. backgroundColor: `rgba(${colors[index % colors.length]}, 0.05)`,
  61. }"
  62. >
  63. <div class="back-item-head">
  64. <div
  65. :style="{
  66. backgroundColor: `rgba(${colors[index % colors.length]}, 1)`,
  67. }"
  68. >
  69. <SvgIcon name="box" color="#ffffff" size="15" />
  70. </div>
  71. Chunk-{{ index + 1 }}·{{ item.wordCount }} 字符
  72. </div>
  73. <div class="mt-2 text-[1.25rem] font-bold text-[#21262D]">
  74. {{ item.text }}
  75. </div>
  76. <div
  77. class="mt-[var(--czr-gap)] flex flex-wrap gap-2 text-sm text-[#576275]"
  78. >
  79. <template v-for="s in item.keywords?.split(',') || []">
  80. <span>#{{ s }}</span>
  81. </template>
  82. </div>
  83. <div
  84. class="mt-4 h-[1px] w-full"
  85. :style="{
  86. backgroundColor: `rgba(${colors[index % colors.length]}, 0.2)`,
  87. }"
  88. />
  89. <div
  90. class="mt-4 flex items-center gap-2 text-[0.75rem] text-[#576275]"
  91. >
  92. <img
  93. :src="DictionaryStore.getFileIcon(item.name)"
  94. class="h-4"
  95. />
  96. {{ item.name }}
  97. </div>
  98. </div>
  99. </template>
  100. </div>
  101. </div>
  102. </div>
  103. </div>
  104. <CzrDialog
  105. :show="state.modelConfig.show"
  106. title="检索方式"
  107. @onClose="state.modelConfig.show = false"
  108. @onSubmit="onSubmitModelConfig"
  109. width="62.5rem"
  110. height="auto"
  111. max-height="90%"
  112. >
  113. <CzrForm ref="ref_form" label-width="6.1rem">
  114. <modelConfig
  115. :index-method="false"
  116. :embedding="false"
  117. ref="ref_modelConfig"
  118. />
  119. </CzrForm>
  120. </CzrDialog>
  121. </template>
  122. <script setup lang="ts">
  123. import { getCurrentInstance, nextTick, onMounted, reactive, ref } from 'vue'
  124. import { v4 } from 'uuid'
  125. import modelConfig from '@/views/manage/knowledge/model-config.vue'
  126. import { SearchMethodTypeMap } from '@/types/knowledge'
  127. import { datasetsCallback, queriesAnswer } from '@/api/modules/knowledge/test'
  128. import { ElMessage } from 'element-plus'
  129. import { useDictionaryStore } from '@/stores'
  130. const DictionaryStore = useDictionaryStore()
  131. const emit = defineEmits(['refresh'])
  132. const props = defineProps({
  133. knowledge: <any>{},
  134. })
  135. const { proxy }: any = getCurrentInstance()
  136. const colors = ['46, 155, 62', '0, 159, 188', '119, 69, 222', '255, 162, 84']
  137. const state: any = reactive({
  138. loading: false,
  139. text: '',
  140. backList: [],
  141. query: {
  142. loading: false,
  143. head: [
  144. { value: 'source', label: '数据源', show: true },
  145. { value: 'content', label: '文本', show: true },
  146. {
  147. value: 'createTime',
  148. label: '时间',
  149. show: true,
  150. datetime: true,
  151. width: 180,
  152. },
  153. ],
  154. page: {
  155. pageNum: 1,
  156. pageSize: 20,
  157. },
  158. result: {
  159. total: 0,
  160. data: [],
  161. },
  162. },
  163. modelConfig: {
  164. show: false,
  165. indexConfig: null,
  166. },
  167. })
  168. const ref_modelConfig = ref()
  169. const ref_form = ref()
  170. const onPage = (pageNum, pageSize) => {
  171. state.query.page = {
  172. pageNum: pageNum,
  173. pageSize: pageSize,
  174. }
  175. const params = {
  176. datesetId: props.knowledge.id,
  177. page: state.query.page.pageNum,
  178. size: state.query.page.pageSize,
  179. }
  180. state.query.loading = true
  181. queriesAnswer(params)
  182. .then(({ data }: any) => {
  183. state.query.result.total = data.totalElements
  184. state.query.result.data = data.content
  185. })
  186. .catch(() => {})
  187. .finally(() => {
  188. state.query.loading = false
  189. })
  190. }
  191. const onModelConfig = () => {
  192. state.modelConfig.show = true
  193. nextTick(() => {
  194. ref_modelConfig.value.init({ indexConfig: state.modelConfig.indexConfig })
  195. })
  196. }
  197. const onSubmitModelConfig = () => {
  198. ref_form.value.submit().then(() => {
  199. state.modelConfig.indexConfig = ref_modelConfig.value.getData().indexConfig
  200. state.modelConfig.show = false
  201. })
  202. }
  203. const onTest = () => {
  204. if (!state.text.trim()) {
  205. ElMessage.warning('请输入源文本')
  206. return
  207. }
  208. state.loading = true
  209. datasetsCallback({
  210. datasetId: props.knowledge.id,
  211. indexConfig: JSON.stringify(state.modelConfig.indexConfig),
  212. context: state.text,
  213. })
  214. .then(({ data }: any) => {
  215. state.backList = data
  216. })
  217. .catch(() => {})
  218. .finally(() => {
  219. state.loading = false
  220. })
  221. }
  222. onMounted(() => {
  223. onPage(1, 10)
  224. state.modelConfig.indexConfig = JSON.parse(
  225. JSON.stringify(props.knowledge.indexConfig),
  226. )
  227. })
  228. </script>
  229. <style lang="scss" scoped>
  230. .bm-main-box {
  231. padding-left: 0;
  232. padding-right: 0;
  233. padding-bottom: 0;
  234. }
  235. .card {
  236. background-color: #ffffff;
  237. box-shadow: 0rem 0.06rem 0.25rem 0rem rgba(38, 110, 255, 0.2);
  238. border: 0.06rem solid #e6e8ea;
  239. padding: 1rem;
  240. }
  241. .text-main {
  242. flex: 1;
  243. display: flex;
  244. flex-direction: column;
  245. border-radius: 0.25rem;
  246. .text-head {
  247. display: flex;
  248. align-items: center;
  249. padding: 0 1.5rem;
  250. background-image: url('@/assets/images/knowledge/knowledge-back-test.png');
  251. background-repeat: no-repeat;
  252. background-size: 100% 100%;
  253. height: 3.75rem;
  254. border-radius: 0.25rem 0.25rem 0 0;
  255. border: var(--czr-border);
  256. font-weight: bold;
  257. font-size: 1.25rem;
  258. color: #303133;
  259. > div {
  260. margin-left: auto;
  261. width: 5.63rem;
  262. height: 1.75rem;
  263. background: #ebf2ff;
  264. border-radius: 0.25rem;
  265. border: 1px solid rgba(var(--czr-main-color-rgb), 0.3);
  266. display: flex;
  267. align-items: center;
  268. justify-content: center;
  269. font-size: 0.75rem;
  270. color: var(--czr-main-color);
  271. }
  272. }
  273. .text-content {
  274. flex: 1;
  275. position: relative;
  276. :deep(.czr-form-column) {
  277. width: 100%;
  278. height: 100%;
  279. .el-form-item {
  280. width: 100%;
  281. height: 100%;
  282. .el-textarea__inner {
  283. width: 100%;
  284. height: 100%;
  285. padding: 0.75rem;
  286. resize: none;
  287. border-top-left-radius: 0;
  288. border-top-right-radius: 0;
  289. }
  290. }
  291. }
  292. .test {
  293. position: absolute;
  294. right: 1.5rem;
  295. bottom: 0.75rem;
  296. }
  297. }
  298. }
  299. .back-item {
  300. border-radius: 0.5rem;
  301. width: 100%;
  302. padding: 2rem 1rem 1rem;
  303. position: relative;
  304. .back-item-head {
  305. position: absolute;
  306. top: 0;
  307. left: 0;
  308. display: flex;
  309. align-items: center;
  310. font-size: 0.75rem;
  311. color: #576275;
  312. gap: 0.5rem;
  313. > div:first-child {
  314. width: 2.5rem;
  315. height: 1.63rem;
  316. box-shadow: 0 0.13rem 0.13rem 0 rgba(0, 0, 0, 0.25);
  317. border-radius: 0.25rem 0 0.25rem 0;
  318. display: flex;
  319. align-items: center;
  320. justify-content: center;
  321. }
  322. }
  323. }
  324. </style>