434 lines
9.6 KiB
Vue
434 lines
9.6 KiB
Vue
<template>
|
|
<div class="agent-item">
|
|
<div
|
|
class="message-wrapper flex"
|
|
:class="{ 'is-user': content.isUser, 'is-loading': content.loading }"
|
|
>
|
|
<div class="thumb">
|
|
<img :src="content.isUser ? userAvatar : agentThumb" class="thumb-icon" />
|
|
</div>
|
|
<div class="message-context" v-show="!content.loading">
|
|
<div class="img-list flex" v-if="imageList.length > 0">
|
|
<img
|
|
v-for="(item, index) in imageList"
|
|
:key="'img-' + index"
|
|
:src="item"
|
|
class="img-item"
|
|
/>
|
|
</div>
|
|
<!-- <div class="thinking-wrapper" v-show="content.thinkingText?.length > 0">
|
|
<div class="thinking">
|
|
<div
|
|
class="thinking-header flex align-center space-between"
|
|
@click="toggleThinkingCollapsed"
|
|
>
|
|
<div class="left flex align-center">
|
|
<span class="think-title" v-show="content.thinking">{{
|
|
t('agent.thinking')
|
|
}}</span>
|
|
<SvgIcon
|
|
v-show="!content.thinking"
|
|
class="think-icon"
|
|
name="checked"
|
|
size="12"
|
|
color="#FF7A51"
|
|
/>
|
|
<span class="think-title" v-show="!content.thinking">{{
|
|
t('agent.thinkComplete')
|
|
}}</span>
|
|
</div>
|
|
<SvgIcon
|
|
:class="{ reverse: thinkingCollapsed }"
|
|
name="arrowDown"
|
|
size="16"
|
|
color="#FF7A51"
|
|
/>
|
|
</div>
|
|
<div class="thinking-content" v-show="!thinkingCollapsed">
|
|
<pre>{{ content.thinkingText }}</pre>
|
|
</div>
|
|
</div>
|
|
</div> -->
|
|
<div class="message-txt markdown-body flex flex-col">
|
|
<VueMarkdown
|
|
:custom-attrs="customAttrs"
|
|
:markdown="content.text"
|
|
:rehype-plugins="[rehypeRaw]"
|
|
>
|
|
<template v-slot:s-card="{ children: children, ...attrs }">
|
|
<Card
|
|
:title="content.reportName ?? attrs.title"
|
|
@click.native="handleClickReport"
|
|
/>
|
|
</template>
|
|
<template v-slot:s-url="{ children: children }">
|
|
<Url :list="content.webAddress" @click.native="handleClickUrls" />
|
|
</template>
|
|
<template v-slot:s-sketch="{ children: children }">
|
|
<Sketch @click.native="handleClickSketch" />
|
|
</template>
|
|
</VueMarkdown>
|
|
<div
|
|
class="web-address flex align-center"
|
|
v-show="content.webAddress?.length > 0"
|
|
>
|
|
<img src="@/assets/images/search.png" class="search-icon" />
|
|
<span>{{ content.webAddress?.length }} web pages have been retrieved.</span>
|
|
</div>
|
|
</div>
|
|
<Pause v-show="showStop && isLast" />
|
|
<div
|
|
v-show="!content.streaming"
|
|
class="operate flex"
|
|
:class="{ 'is-user': content.isUser }"
|
|
>
|
|
<template v-if="content.isUser">
|
|
<SvgIcon name="copy" size="16" color="#000" @click.stop="handleCopyText" />
|
|
</template>
|
|
<template v-else>
|
|
<SvgIcon
|
|
v-for="operate in operateList"
|
|
:key="operate.name"
|
|
v-show="isLast"
|
|
:name="operate.name"
|
|
:size="operate.name === 'refreshTransparent' ? '14' : '16'"
|
|
color="#000000A6"
|
|
@click.stop="operate.action"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div class="message-context loading" v-show="content.loading">
|
|
<video
|
|
src="@/assets/images/generate-loading.mp4"
|
|
autoplay
|
|
loop
|
|
muted
|
|
class="loading-gif"
|
|
></video>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import userThumb from '@/assets/images/user-thumb.jpg'
|
|
import agentThumb from '@/assets/images/agent-thumb.png'
|
|
import Card from './ReportCard.vue'
|
|
import Url from './UrlCard.vue'
|
|
import Sketch from './SketchCard.vue'
|
|
import Pause from './Pause.vue'
|
|
import gsap from 'gsap'
|
|
import { VueMarkdown } from '@crazydos/vue-markdown'
|
|
import type { CustomAttrs } from '@crazydos/vue-markdown'
|
|
import rehypeRaw from 'rehype-raw'
|
|
import MyEvent from '@/utils/myEvent'
|
|
import { useUserInfoStore } from '@/stores'
|
|
|
|
const userStore = useUserInfoStore()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const props = defineProps<{
|
|
content: Object
|
|
isLast: Boolean
|
|
}>()
|
|
|
|
const emit = defineEmits(['regenerate'])
|
|
|
|
const userAvatar = computed(() => {
|
|
return userStore.state.userInfo?.avatar || userThumb
|
|
})
|
|
|
|
const imageList = computed(() => {
|
|
const { imageUrls, role } = props.content
|
|
const list = []
|
|
if (role === 'user') {
|
|
const quotaList = props.content.image_url ?? []
|
|
list.push(...quotaList)
|
|
}
|
|
if (!imageUrls || imageUrls.length === 0) return list
|
|
imageUrls.forEach((item) => {
|
|
if (typeof item === 'string') {
|
|
list.push(item)
|
|
} else if (typeof item === 'object' && item.url) {
|
|
list.push(item.url)
|
|
}
|
|
})
|
|
return list
|
|
})
|
|
|
|
const customAttrs: CustomAttrs = {
|
|
img: {
|
|
style: 'max-width: 100%;'
|
|
},
|
|
a: (node, combinedAttrs) => {
|
|
if (typeof node.properties.href === 'string') {
|
|
return { target: '_blank', rel: 'noopener noreferrer' }
|
|
} else {
|
|
return {}
|
|
}
|
|
},
|
|
heading: {
|
|
style: {
|
|
fontFamily: 'Regular',
|
|
lineHeight: 2
|
|
// fontSize: '1.4rem'
|
|
}
|
|
},
|
|
p: {
|
|
style: {
|
|
fontSize: '1.4rem',
|
|
lineHeight: 1.5
|
|
}
|
|
}
|
|
}
|
|
|
|
const operateList = ref([
|
|
{
|
|
name: 'thumbUp',
|
|
action: () => {
|
|
console.log('thumbUp')
|
|
}
|
|
},
|
|
{
|
|
name: 'thumbDown',
|
|
action: () => {
|
|
console.log('thumbDown')
|
|
}
|
|
},
|
|
{
|
|
name: 'refreshTransparent',
|
|
action: () => {
|
|
// emit('regenerate')
|
|
}
|
|
},
|
|
{
|
|
name: 'copy',
|
|
action: () => {
|
|
handleCopyText()
|
|
}
|
|
}
|
|
])
|
|
|
|
const loading = ref(false)
|
|
|
|
const handleCopyText = () => {
|
|
const text = props.content.text
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard
|
|
.writeText(props.content.text)
|
|
.then(() => {
|
|
// console.log('Text copied to clipboard');
|
|
ElMessage({
|
|
message: t('agent.copySuccess'),
|
|
type: 'success',
|
|
offset: 300
|
|
})
|
|
})
|
|
.catch((err) => {
|
|
console.error('Could not copy text: ', err)
|
|
ElMessage({
|
|
message: t('agent.copyFailed'),
|
|
type: 'error',
|
|
offset: 300
|
|
})
|
|
})
|
|
} else {
|
|
var textarea = document.createElement('textarea')
|
|
document.body.appendChild(textarea)
|
|
// 隐藏此输入框
|
|
textarea.style.position = 'fixed'
|
|
textarea.style.clip = 'rect(0 0 0 0)'
|
|
textarea.style.top = '10px'
|
|
// 赋值
|
|
textarea.value = text
|
|
// 选中
|
|
textarea.select()
|
|
// 复制
|
|
document.execCommand('copy', true)
|
|
// 移除输入框
|
|
document.body.removeChild(textarea)
|
|
ElMessage({
|
|
message: t('agent.copySuccess'),
|
|
type: 'success',
|
|
offset: 300
|
|
})
|
|
}
|
|
}
|
|
const thinkingCollapsed = ref(true)
|
|
const toggleThinkingCollapsed = () => {
|
|
thinkingCollapsed.value = !thinkingCollapsed.value
|
|
}
|
|
|
|
const handleClickReport = () => {
|
|
MyEvent.emit('openReport', props.content)
|
|
// 点击显示报告
|
|
}
|
|
const handleClickUrls = (data) => {
|
|
MyEvent.emit('openUrls', props.content.webAddress)
|
|
// 点击显示来源
|
|
}
|
|
const handleClickSketch = () => {
|
|
MyEvent.emit('openSketch')
|
|
}
|
|
|
|
const showStop = ref(false)
|
|
const handleStopChat = () => {
|
|
if (props.isLast) {
|
|
showStop.value = true
|
|
}
|
|
|
|
// props.content.loading = false
|
|
}
|
|
onMounted(() => {
|
|
MyEvent.add('stopChat', handleStopChat)
|
|
})
|
|
onUnmounted(() => {
|
|
MyEvent.remove('stopChat', handleStopChat)
|
|
})
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.c-svg {
|
|
width: initial;
|
|
cursor: pointer;
|
|
}
|
|
.agent-item {
|
|
font-family: 'Regular';
|
|
font-size: 1.4rem;
|
|
.message-wrapper {
|
|
column-gap: 0.9rem;
|
|
&.is-user {
|
|
flex-direction: row-reverse;
|
|
column-gap: 1.3rem;
|
|
}
|
|
&.is-loading {
|
|
align-items: center;
|
|
}
|
|
|
|
.thumb {
|
|
flex-shrink: 0;
|
|
width: 4.4rem;
|
|
height: 4.4rem;
|
|
border-radius: 50%;
|
|
border: 0.1rem solid #e5dfdf;
|
|
position: relative;
|
|
z-index: 2;
|
|
.thumb-icon {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
}
|
|
}
|
|
.message-context {
|
|
line-height: 2rem;
|
|
font-size: 1.4rem;
|
|
width: 82%;
|
|
}
|
|
&.is-user .message-context {
|
|
width: fit-content;
|
|
max-width: 82%;
|
|
}
|
|
.web-address {
|
|
width: fit-content;
|
|
min-width: 22.5rem;
|
|
line-height: 2.6rem;
|
|
padding: 0 1rem;
|
|
border-radius: 1.5rem;
|
|
color: #000000a6;
|
|
border: 0.1rem solid #0000001a;
|
|
font-family: 'Regular';
|
|
font-weight: 400;
|
|
font-size: 1.2rem;
|
|
margin-top: 1rem;
|
|
column-gap: 0.8rem;
|
|
.search-icon {
|
|
width: 1.4rem;
|
|
height: 1.4rem;
|
|
}
|
|
}
|
|
}
|
|
.operate {
|
|
margin-top: 1.3rem;
|
|
column-gap: 1.2rem;
|
|
}
|
|
}
|
|
.img-list {
|
|
column-gap: 1rem;
|
|
margin-bottom: 1.4rem;
|
|
justify-content: flex-end;
|
|
.img-item {
|
|
width: 6.8rem;
|
|
height: 6.8rem;
|
|
border: 0.1rem solid #cdcdcd;
|
|
border-radius: 1.5rem;
|
|
object-fit: contain;
|
|
}
|
|
}
|
|
|
|
.loading-gif {
|
|
height: 10rem;
|
|
position: relative;
|
|
margin-left: -2.4rem;
|
|
}
|
|
|
|
.thinking {
|
|
color: #ff7a51;
|
|
font-family: 'Medium';
|
|
margin-bottom: 1.6rem;
|
|
border: 0.1rem solid transparent;
|
|
border-radius: 0.4rem;
|
|
background: linear-gradient(#fffcf4, #fffcf4),
|
|
linear-gradient(
|
|
119.03deg,
|
|
rgba(233, 121, 60, 0.3) 1.61%,
|
|
rgba(255, 207, 144, 0.3) 101.01%
|
|
);
|
|
background-origin: padding-box, border-box;
|
|
background-clip: padding-box, border-box;
|
|
|
|
.thinking-header {
|
|
cursor: pointer;
|
|
padding: 0.8rem 1.2rem;
|
|
.think-icon {
|
|
margin-right: 0.6rem;
|
|
}
|
|
.reverse {
|
|
transform: rotate(180deg);
|
|
transition: transform 0.3s ease-in-out;
|
|
}
|
|
}
|
|
|
|
.thinking-content {
|
|
padding: 0 1.4rem 1.6rem;
|
|
pre {
|
|
color: #f6a478;
|
|
margin: 0;
|
|
border-left: 0.035rem solid #f6a478;
|
|
white-space: pre-wrap;
|
|
font-family: inherit;
|
|
font-size: 1.2rem;
|
|
padding-left: 1.2rem;
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="less">
|
|
.message-txt {
|
|
user-select: text;
|
|
ul {
|
|
list-style-position: inside;
|
|
}
|
|
code {
|
|
white-space: pre-wrap;
|
|
}
|
|
img {
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
</style>
|