|
@@ -1,16 +1,19 @@
|
|
|
<template>
|
|
|
- <div class="answer">
|
|
|
- <div class="answer-avatar">
|
|
|
- <img src="@/views/smart-ask-answer/assistant-2/imgs/avatar.png"/>
|
|
|
+ <div class="flex gap-2">
|
|
|
+ <div class="h-11 w-11">
|
|
|
+ <img class="h-full w-full" src="@/assets/images/chat/default-logo.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>
|
|
|
+ <div class="flex flex-1 overflow-hidden">
|
|
|
+ <template v-if="item.prologue">
|
|
|
+ <div
|
|
|
+ class="rounded-lg rounded-tl-none bg-[#EAF1FF] p-2.75 text-[#303133]"
|
|
|
+ style="line-height: 1.5"
|
|
|
+ v-html="item.prologue"
|
|
|
+ />
|
|
|
+ <template v-if="item.prologueQuestions?.length > 0">
|
|
|
+ <template v-if="item.prologueType === 'three'"></template>
|
|
|
+ </template>
|
|
|
+ <template v-for="pq in item.prologueQuestions"> </template>
|
|
|
</template>
|
|
|
<template v-else-if="item.loading">
|
|
|
<div class="answer-loading">
|
|
@@ -20,147 +23,94 @@
|
|
|
</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>
|
|
|
+ <div
|
|
|
+ class="rounded-lg rounded-tl-none bg-[#EAF1FF] p-2.75 text-[#303133]"
|
|
|
+ >
|
|
|
+ <template v-for="part in textCpt">
|
|
|
+ <template v-if="part.type === 'think'">
|
|
|
+ <thinkCom :text="part.text" />
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div
|
|
|
+ class="answer-markdown"
|
|
|
+ v-html="md.render(part.text.replace(/^<think>/, ''))"
|
|
|
+ />
|
|
|
</template>
|
|
|
-<!-- <div class="answer-markdown" v-html="DOMPurify.sanitize(marked.parse(part.content))"/>-->
|
|
|
</template>
|
|
|
- </template>
|
|
|
+ </div>
|
|
|
+ <div></div>
|
|
|
+ <div></div>
|
|
|
</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 { 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';
|
|
|
+import { copy } from '@/utils/czr-util'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import useTextToSpeech from './useTextToSpeech'
|
|
|
|
|
|
-const { speak, stop } = useTextToSpeech();
|
|
|
+const { speak, stop } = useTextToSpeech()
|
|
|
const md = new MarkdownIt({
|
|
|
- html: false, // 在源码中启用 HTML 标签
|
|
|
+ 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>`;
|
|
|
+ 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>`;
|
|
|
- }
|
|
|
-});
|
|
|
+ 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 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
|
|
|
+const textCpt = computed(() => {
|
|
|
+ const segments: any = []
|
|
|
+ const rawContent = props.item.text
|
|
|
|
|
|
// 正则表达式匹配<think>标签及其内容
|
|
|
- const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
|
|
|
+ const thinkRegex = /<think>([\s\S]*?)<\/think>/g
|
|
|
|
|
|
- let match;
|
|
|
- let lastIndex = 0;
|
|
|
+ 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)
|
|
|
- });
|
|
|
+ text: rawContent.substring(lastIndex, match.index),
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
// 添加think内容
|
|
|
segments.push({
|
|
|
type: 'think',
|
|
|
- content: match[1].trim()
|
|
|
- });
|
|
|
+ text: match[1].trim(),
|
|
|
+ })
|
|
|
|
|
|
- lastIndex = thinkRegex.lastIndex;
|
|
|
+ lastIndex = thinkRegex.lastIndex
|
|
|
}
|
|
|
|
|
|
// 添加剩余内容
|
|
|
if (lastIndex < rawContent.length) {
|
|
|
segments.push({
|
|
|
type: 'response',
|
|
|
- content: rawContent.substring(lastIndex).trim()
|
|
|
- });
|
|
|
+ text: rawContent.substring(lastIndex).trim(),
|
|
|
+ })
|
|
|
}
|
|
|
- return segments;
|
|
|
+ return segments
|
|
|
})
|
|
|
const onCopy = (text) => {
|
|
|
copy(text)
|
|
@@ -169,100 +119,50 @@ const onCopy = (text) => {
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.answer {
|
|
|
- width: 100%;
|
|
|
- display: flex;
|
|
|
- .answer-avatar {
|
|
|
- margin-right: 16px;
|
|
|
+.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: #6c9eff;
|
|
|
+ animation: ellipsis-bounce 1.4s infinite ease-in-out;
|
|
|
}
|
|
|
- .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;
|
|
|
- }
|
|
|
+ span:nth-child(1) {
|
|
|
+ animation-delay: -0.32s;
|
|
|
+ }
|
|
|
|
|
|
- @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;
|
|
|
- }
|
|
|
- }
|
|
|
+ span:nth-child(2) {
|
|
|
+ animation-delay: -0.16s;
|
|
|
}
|
|
|
- .answer-markdown {
|
|
|
- line-height: 1.8;
|
|
|
- >* {
|
|
|
- margin-bottom: 12px;
|
|
|
- &:last-child {
|
|
|
- margin-bottom: 0;
|
|
|
- }
|
|
|
+
|
|
|
+ @keyframes ellipsis-bounce {
|
|
|
+ 0%,
|
|
|
+ 80%,
|
|
|
+ 100% {
|
|
|
+ transform: translateY(0);
|
|
|
}
|
|
|
- }
|
|
|
- .answer-operation {
|
|
|
- margin-top: 16px;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- .answer-operation-item {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
+ 40% {
|
|
|
+ transform: translateY(-10px);
|
|
|
}
|
|
|
}
|
|
|
- .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;
|
|
|
+}
|
|
|
+.answer-markdown {
|
|
|
+ line-height: 1.5;
|
|
|
+ > * {
|
|
|
+ text-align: justify;
|
|
|
+ line-height: inherit;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
}
|
|
|
}
|
|
|
}
|