From 50f811399f22b835fd2fc33375fe01ff8eb83dd8 Mon Sep 17 00:00:00 2001 From: wujingjing <gersonwu@qq.com> Date: 星期一, 21 十月 2024 13:50:04 +0800 Subject: [PATCH] 查询高亮指标 --- src/components/chat/components/playBar/PlayBar.vue | 364 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 files changed, 351 insertions(+), 13 deletions(-) diff --git a/src/components/chat/components/playBar/PlayBar.vue b/src/components/chat/components/playBar/PlayBar.vue index 129c347..28f6947 100644 --- a/src/components/chat/components/playBar/PlayBar.vue +++ b/src/components/chat/components/playBar/PlayBar.vue @@ -5,18 +5,63 @@ <img src="/static/images/wave/PlugIn.png" class="set-icon box-border" /> </el-button> </div> + <InfoDetail class="text-base" v-model="infoDetailIsShow" :item="detailMapRow" /> + <div class="set-input"> - <el-input @keydown.enter="isTalking || emits('sendClick')" v-model="inputValue" placeholder="鍦ㄨ繖閲岃緭鍏ユ偍鐨勯棶棰樺紑濮嬪拰AI瀵硅瘽" class="set-inputAnswer" /> + <!-- @input="inputText" --> + + <el-input + ref="inputRef" + class="relative align-bottom set-inputAnswer" + type="textarea" + resize="none" + :autosize="{ minRows: 1, maxRows: 8 }" + v-elInputFocus + @keydown="keydownInput" + @input="inputText" + v-model="inputValue" + placeholder="鍦ㄨ繖閲岃緭鍏ユ偍鐨勯棶棰樺紑濮嬪拰AI瀵硅瘽" + > + </el-input> + <div + v-show="tipIsShow" + ref="tipEleRef" + class="absolute rounded-md bg-white border border-solid border-gray-400 py-2 z-10" + :style="{ left: popUpPosition.left + 'px', bottom: popUpPosition.bottom + 'px' }" + > + <div class="font-bold text-sm text-nowrap overflow-hidden text-ellipsis max-w-80 mb-1 px-2">Ctrl+鏁板瓧蹇嵎杈撳叆</div> + <div class="text-gray-400 text-sm text-nowrap overflow-hidden text-ellipsis max-w-80 mb-1 px-2">{{ inputValue }}</div> + <div class="max-w-96 flex flex-col"> + <div + class="hover:bg-gray-300 py-2 cursor-pointer px-5 text-nowrap overflow-hidden text-ellipsis" + v-for="(item, index) in similarList" + :key="index" + @click="similarClick(item)" + > + <span class="text-sm text-gray-500 pr-1.5">{{ index + 1 }}</span> + <template v-if="sentenceSplitMap?.[item.question]"> + <template v-for="part in sentenceSplitMap[item.question]"> + <span + v-if="part.isKeyword" + class="text-blue-400 font-bold cursor-pointer" + >{{ part.partStr }}</span + > + <span v-else>{{ part.partStr }}</span> + </template> + </template> + </div> + </div> + </div> </div> <div class="h100 flex items-center"> <div class="upload_img space-y"> <div class="imgbox cursor-pointer flex items-center"> - <el-button title="AI鐪嬪浘" class="cursor-pointer" link style="margin-left: unset"> + <!-- <el-button title="AI鐪嬪浘" class="cursor-pointer" link style="margin-left: unset"> <img src="/static/images/wave/LookImg.png" class="set-img-icon box-border" /> - </el-button> - <el-button title="AI璇煶瀵硅瘽" class="cursor-pointer" link style="margin-left: unset"> + </el-button> --> + <!-- <el-button title="AI璇煶瀵硅瘽" class="cursor-pointer" link style="margin-left: unset" @click="audioChangeWord"> <img src="/static/images/wave/HeadImg.png" class="set-img-icon box-border" /> - </el-button> + </el-button> --> <el-button title="鍙戦��" :disabled="isTalking" class="cursor-pointer" link @click="emits('sendClick')"> <div class="send"> @@ -26,19 +71,310 @@ </div> </div> </div> + <VoicePage + v-model:isShow="voicePageIsShow" + v-show="voicePageIsShow" + @submit="(cb) => emits('sendClick', cb)" + @updateInputValue="updateInputValue" + :isHome="isHome" + /> </div> </template> <script setup lang="ts"> -import { reactive } from 'vue'; +import type { InputInstance } from 'element-plus'; +import { ElMessage } from 'element-plus'; +import getCaretCoordinates from 'textarea-caret'; +import { computed, nextTick, ref } from 'vue'; +import InfoDetail from './InfoDetail.vue'; +import VoicePage from './voicePage/VoicePage.vue'; +import { getMetricsNames, querySimilarityHistory } from '/@/api/ai/chat'; +import { onClickOutside } from '@vueuse/core'; +import _ from 'lodash'; const emits = defineEmits(['sendClick']); - -defineProps(['isTalking']) - +const props = defineProps(['isTalking', 'isHome']); +const voicePageIsShow = defineModel('voicePageIsShow', { + type: Boolean, + default: false, +}); const inputValue = defineModel({ - type:String -}) + type: String, +}); + +const tipIsShow = computed(() => !!inputValue.value.trim() && similarList.value?.length > 0 && triggerShow.value); +const triggerShow = ref(false); +const inputRef = ref<InputInstance>(null); + +const updateInputValue = (val) => { + inputValue.value = val; +}; +const keydownInput = (e) => { + if (props.isTalking) return; + const isEnterInput = !e.shiftKey && e.key == 'Enter'; + const isDigitalInput = e.ctrlKey && e.code.startsWith('Digit') && tipIsShow.value; + if (isEnterInput || isDigitalInput) { + e.cancelBubble = true; //ie闃绘鍐掓场琛屼负 + e.stopPropagation(); //Firefox闃绘鍐掓场琛屼负 + e.preventDefault(); //鍙栨秷浜嬩欢鐨勯粯璁ゅ姩浣�*鎹㈣ + if (isEnterInput) { + //浠ヤ笅澶勭悊鍙戦�佹秷鎭唬鐮� + emits('sendClick'); + } else if (isDigitalInput) { + const num = Number(e.code.replace('Digit', '')); + const mapValue = similarList.value[num - 1]?.question; + if (mapValue) { + inputValue.value = mapValue; + triggerShow.value = false; + } + } + } +}; + +const similarClick = (item) => { + if (item.question) { + inputValue.value = item.question; + triggerShow.value = false; + } +}; + +const tipEleRef = ref<HTMLDivElement>(null); + +const popUpPosition = ref({ + left: null, + bottom: null, +}); + +onClickOutside(tipEleRef, () => { + triggerShow.value = false; +}); +const inputText = (text) => { + nextTick(() => { + setTimeout(() => { + const container = inputRef.value.$el; + + const textAreaEl = inputRef.value.$el.firstElementChild; + const caret = getCaretCoordinates(textAreaEl, textAreaEl.selectionEnd); + + const bottomOffset = 10; + const leftOffset = 9; + popUpPosition.value.left = caret.left + leftOffset; + popUpPosition.value.bottom = container.offsetHeight + bottomOffset; + triggerShow.value = true; + + if (lastIsFinish) { + querySimilarityApi(text); + } + }, 0); + }); +}; +// 瀵� question 杩涜鍒嗗壊 +const sentenceSplitMap = ref< + Record< + string, + { + partStr: string; + startIndex: number; + endIndex: number; + isKeyword: boolean; + }[] + > +>({}); +const similarList = ref([]); +let lastIsFinish = true; +const querySimilarityApi = async (text: string) => { + if (!text) return; + lastIsFinish = false; + const res = await querySimilarityHistory({ + question: text, + }); + lastIsFinish = true; + const handleValues = res?.values ?? []; + + similarList.value = props.isHome ? handleValues.slice(0, 3) : handleValues; + metricsNamesPromise.then((value) => { + sentenceSplitMap.value = getSentenceMatchMap( + similarList.value.map((item) => item.question), + value as string[] + ); + }); +}; +const audioChangeWord = () => { + navigator.getUserMedia( + { audio: true }, + function onSuccess(stream) { + voicePageIsShow.value = true; + }, + function onError(error) { + ElMessage.warning('璇锋墦寮�楹﹀厠椋庢潈闄�'); + } + ); +}; +/** + * 鍒囧垎鍙ュ瓙锛屽尮閰嶈瘝鐢� isKeyword 鏍囪 + * @param sentences + * @param keywords + */ +const getSentenceMatchMap = (sentences: string[], keywords: any[]) => { + if (!sentences || sentences.length === 0) return null; + if (!keywords || keywords.length===0) { + return sentences.map((item) => ({ + partStr: item, + startIndex: 0, + endIndex: item.length, + isKeyword: false, + })); + } + let sentenceMatchMap = {}; + + sentences.map((sentence) => { + if (!sentenceMatchMap[sentence]) { + let sentenceMatchList = []; + keywords.map((keyword) => { + const matchList = [...sentence.matchAll(keyword)].map((item) => { + return { + partStr: item[0], + startIndex: item.index, + endIndex: item.index + item[0].length, + }; + }); + sentenceMatchList = sentenceMatchList.concat(matchList); + }); + + let nextIsMerge = false; + const checkNextIsMerge = (value, index, array) => { + nextIsMerge = false; + if (index === array.length - 1) return; + const nextValue = array[index + 1]; + + // 閫氳繃 nextIsMerge 鎺у埗涓嬩竴鍏冪礌鏄惁闇�瑕佷娇鐢� + if (value.endIndex > nextValue.startIndex) { + nextIsMerge = true; + } + }; + + // 鎸� startIndex 鎺掑簭锛屾秷闄ゅ郊姝や箣闂撮噸鍚堝厓绱� + sentenceMatchList = _.sortBy(sentenceMatchList, (item) => item.startIndex).filter((value, index, array) => { + if (nextIsMerge) { + checkNextIsMerge(value, index, array); + return false; + } + checkNextIsMerge(value, index, array); + return true; + }); + + sentenceMatchMap[sentence] = sentenceMatchList; + } + }); + + for (const sentence of Object.keys(sentenceMatchMap)) { + const matchList = sentenceMatchMap[sentence]; + const result = []; + + if (matchList.length === 0) { + result.push({ + partStr: sentence, + startIndex: 0, + endIndex: sentence.length, + isKeyword: false, + }); + + sentenceMatchMap[sentence] = result; + + continue; + } + + matchList.forEach((value, index, array) => { + // 鍖归厤璇嶆伆濂戒笉鏄綅浜庣粨鏉熶綅缃� + if (array.length - 1 === index && value.endIndex !== array.length) { + result.push({ + ...value, + isKeyword: true, + }); + + if (value.endIndex !== sentence.length) { + result.push({ + partStr: sentence.slice(value.endIndex, sentence.length), + startIndex: value.endIndex, + endIndex: sentence.length, + isKeyword: false, + }); + } + + // 濡傛灉鏁扮粍鍙湁涓�涓厓绱狅紝鍓嶉潰鐨勪篃闇�瑕佸姞杩涘幓 + if (array.length === 1 && value.startIndex !== 0) { + result.unshift({ + partStr: sentence.slice(0, value.startIndex), + startIndex: 0, + endIndex: value.startIndex, + isKeyword: false, + }); + } + + return; + } + + // 鍖归厤璇嶆伆濂戒笉鏄綅浜庤捣濮嬩綅缃� + if (value.startIndex !== 0 && index === 0) { + result.push({ + ...value, + isKeyword: true, + }); + result.unshift({ + partStr: sentence.slice(0, value.startIndex), + startIndex: 0, + endIndex: value.startIndex, + isKeyword: false, + }); + + return; + } + + // 鎭板ソ浣嶄簬绗竴涓� + if (index === 0) { + result.push({ + ...value, + isKeyword: true, + }); + return; + } + + // 涓棿鏈夐潪鍏抽敭璇� + if (array[index - 1].endIndex !== value.startIndex) { + result.push({ + partStr: sentence.slice(array[index - 1].endIndex, value.startIndex), + startIndex: array[index - 1].endIndex, + endIndex: value.startIndex, + isKeyword: false, + }); + + result.push({ + ...value, + isKeyword: true, + }); + } + }); + + sentenceMatchMap[sentence] = result; + } + + return sentenceMatchMap; +}; + +const metricsNamesPromise = new Promise(async (resolve, reject) => { + const metricNames = (await getMetricsNames())?.values ?? []; + resolve(metricNames); +}); + +//#region ====================== 楂樹寒鎸囨爣鐐瑰嚮====================== +const infoDetailIsShow = ref(false); +const detailMapRow = ref(null); + +const tipMetricsClick = (row) => { + detailMapRow.value = row; + infoDetailIsShow.value = true; +}; +//#endregion </script> <style scoped lang="scss"> .set-waterTitle { @@ -127,14 +463,16 @@ display: inline-block; width: 100%; .set-inputAnswer { - min-height: 36px; - height: 36px; padding: 3px 0; line-height: 20px; border: none; background-color: transparent; color: #333; font-size: 15px; + :deep(.el-textarea__inner) { + // 鍘婚櫎绾� + box-shadow: none; + } } :deep(.el-input__wrapper) { box-shadow: unset; -- Gitblit v1.9.3