|
@@ -0,0 +1,269 @@
|
|
|
+<template>
|
|
|
+ <div class="answer">
|
|
|
+ <div class="answer-avatar">
|
|
|
+ <img src="@/views/smart-ask-answer/assistant-2/imgs/avatar.png"/>
|
|
|
+ </div>
|
|
|
+ <div class="answer-content">
|
|
|
+ <template v-if="item.welcome">
|
|
|
+ <div class="answer-content-text">{{ item.content }}</div>
|
|
|
+ <div class="answer-content-question" v-if="item.question?.length > 0">
|
|
|
+ <template v-for="ques in item.question">
|
|
|
+ <div class="ques-item __hover" @click="$emit('setText', {text: ques, send: true})">{{ques}}</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="item.loading">
|
|
|
+ <div class="answer-loading">
|
|
|
+ <span></span>
|
|
|
+ <span></span>
|
|
|
+ <span></span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <template v-for="part in contentCpt">
|
|
|
+ <template v-if="part.type === 'think'">
|
|
|
+ <thinkCom :content="part.content"/>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="answer-markdown" v-html="md.render(part.content.replace(/^<think>/, ''))"/>
|
|
|
+ <div class="answer-operation">
|
|
|
+ <el-tooltip content="播放" placement="top">
|
|
|
+ <div class="answer-operation-item __hover" @click="speak(part.content)">
|
|
|
+ <img src="@/views/smart-ask-answer/assistant-2/imgs/play.png"/>
|
|
|
+ </div>
|
|
|
+ </el-tooltip>
|
|
|
+ <el-tooltip content="复制" placement="top">
|
|
|
+ <div class="answer-operation-item __hover" @click="onCopy(part.content)">
|
|
|
+ <img src="@/views/smart-ask-answer/assistant-2/imgs/copy.png"/>
|
|
|
+ </div>
|
|
|
+ </el-tooltip>
|
|
|
+ <template v-if="item.messageId">
|
|
|
+ <template v-if="!badMap.has(item.messageId)">
|
|
|
+ <template v-if="goodMap.has(item.messageId)">
|
|
|
+ <el-tooltip content="取消点赞" placement="top">
|
|
|
+ <div class="answer-operation-item __hover" @click="$emit('onNormal', item)">
|
|
|
+ <SvgIcon name="good" color="#2264f0" size="20"/>
|
|
|
+ </div>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <el-tooltip content="点赞" placement="top">
|
|
|
+ <div class="answer-operation-item __hover" @click="$emit('onGood', item)">
|
|
|
+ <SvgIcon name="good" color="#a6a6a6" size="20"/>
|
|
|
+ </div>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ <template v-if="!goodMap.has(item.messageId)">
|
|
|
+ <template v-if="badMap.has(item.messageId)">
|
|
|
+ <el-tooltip content="取消点踩" placement="top">
|
|
|
+ <div class="answer-operation-item __hover" @click="$emit('onNormal', item)">
|
|
|
+ <SvgIcon name="good" color="#d92d20" size="20" rotate="180"/>
|
|
|
+ </div>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <el-tooltip content="点踩" placement="top">
|
|
|
+ <div class="answer-operation-item __hover" @click="$emit('onBad', item)">
|
|
|
+ <SvgIcon name="good" color="#a6a6a6" size="20" rotate="180"/>
|
|
|
+ </div>
|
|
|
+ </el-tooltip>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <template v-if="item.suggest?.length > 0">
|
|
|
+ <div class="answer-suggest">
|
|
|
+ 相似问题
|
|
|
+ <template v-for="sug in item.suggest">
|
|
|
+ <div class="__hover" @click="$emit('setText', {text: sug, send: true})">{{sug}}</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+<!-- <div class="answer-markdown" v-html="DOMPurify.sanitize(marked.parse(part.content))"/>-->
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import {computed, getCurrentInstance, onMounted, reactive, watch} from "vue";
|
|
|
+import MarkdownIt from 'markdown-it';
|
|
|
+import hljs from 'highlight.js';
|
|
|
+import { marked } from 'marked';
|
|
|
+import DOMPurify from 'dompurify';
|
|
|
+import thinkCom from './think.vue'
|
|
|
+import {copy} from "@/utils/czr-util";
|
|
|
+import {ElMessage} from "element-plus";
|
|
|
+import useTextToSpeech from './useTextToSpeech';
|
|
|
+
|
|
|
+const { speak, stop } = useTextToSpeech();
|
|
|
+const md = new MarkdownIt({
|
|
|
+ html: false, // 在源码中启用 HTML 标签
|
|
|
+ highlight: (str, lang) => {
|
|
|
+ if (lang && hljs.getLanguage(lang)) {
|
|
|
+ try {
|
|
|
+ return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang }).value}</code></pre>`;
|
|
|
+ } catch (__) {}
|
|
|
+ }
|
|
|
+ return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`;
|
|
|
+ }
|
|
|
+});
|
|
|
+const props = defineProps({
|
|
|
+ item: {} as any,
|
|
|
+ goodMap: {} as any,
|
|
|
+ badMap: {} as any,
|
|
|
+})
|
|
|
+const state: any = reactive({
|
|
|
+})
|
|
|
+
|
|
|
+const parsedContent = computed(() => {
|
|
|
+ // 先解析Markdown,再净化HTML
|
|
|
+ return DOMPurify.sanitize(marked.parse(props.item.content));
|
|
|
+});
|
|
|
+
|
|
|
+const contentCpt = computed(() => {
|
|
|
+ const segments: any = [];
|
|
|
+ const rawContent = props.item.content
|
|
|
+
|
|
|
+ // 正则表达式匹配<think>标签及其内容
|
|
|
+ const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
|
|
|
+
|
|
|
+ let match;
|
|
|
+ let lastIndex = 0;
|
|
|
+
|
|
|
+ while ((match = thinkRegex.exec(rawContent)) !== null) {
|
|
|
+ // 添加think标签前的普通内容
|
|
|
+ if (match.index > lastIndex) {
|
|
|
+ segments.push({
|
|
|
+ type: 'response',
|
|
|
+ content: rawContent.substring(lastIndex, match.index)
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加think内容
|
|
|
+ segments.push({
|
|
|
+ type: 'think',
|
|
|
+ content: match[1].trim()
|
|
|
+ });
|
|
|
+
|
|
|
+ lastIndex = thinkRegex.lastIndex;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加剩余内容
|
|
|
+ if (lastIndex < rawContent.length) {
|
|
|
+ segments.push({
|
|
|
+ type: 'response',
|
|
|
+ content: rawContent.substring(lastIndex).trim()
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return segments;
|
|
|
+})
|
|
|
+const onCopy = (text) => {
|
|
|
+ copy(text)
|
|
|
+ ElMessage.success('复制成功!')
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.answer {
|
|
|
+ width: 100%;
|
|
|
+ display: flex;
|
|
|
+ .answer-avatar {
|
|
|
+ margin-right: 16px;
|
|
|
+ }
|
|
|
+ .answer-content {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #111111;
|
|
|
+ .answer-loading {
|
|
|
+ width: 50px;
|
|
|
+ height: 100%;
|
|
|
+ display: inline-flex;
|
|
|
+ gap: 6px;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ span {
|
|
|
+ display: inline-block;
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #1d64fd;
|
|
|
+ animation: ellipsis-bounce 1.4s infinite ease-in-out;
|
|
|
+ }
|
|
|
+
|
|
|
+ span:nth-child(1) {
|
|
|
+ animation-delay: -0.32s;
|
|
|
+ }
|
|
|
+
|
|
|
+ span:nth-child(2) {
|
|
|
+ animation-delay: -0.16s;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes ellipsis-bounce {
|
|
|
+ 0%, 80%, 100% {
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+ 40% {
|
|
|
+ transform: translateY(-10px);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .answer-content-text {
|
|
|
+ margin-top: 16px;
|
|
|
+ }
|
|
|
+ .answer-content-question {
|
|
|
+ margin-top: 10px;
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 4px;
|
|
|
+ .ques-item {
|
|
|
+ padding: 6px 14px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #10182824;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #1D64FD;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .answer-markdown {
|
|
|
+ line-height: 1.8;
|
|
|
+ >* {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .answer-operation {
|
|
|
+ margin-top: 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ .answer-operation-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .answer-suggest {
|
|
|
+ margin-top: 14px;
|
|
|
+ padding-top: 14px;
|
|
|
+ border-top: 1px solid #D8DAE5;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #9A9CA6;
|
|
|
+ >div {
|
|
|
+ color: #1D64FD;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|