Files
FiDA_Front/src/views/home/agent/components/Item.vue
2026-03-13 11:35:56 +08:00

312 lines
6.7 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 ? userThumb : agentThumb" class="thumb-icon" />
</div>
<div
class="message-context"
v-show="!content.loading && !content.thinking && !content.streaming"
>
<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="message-txt markdown-body flex flex-col">
<!-- <div v-html="formatMessage"></div> -->
<VueMarkdown
:custom-attrs="customAttrs"
:markdown="content.text"
:rehype-plugins="[rehypeRaw]"
>
<template v-slot:s-ReportCard="" {children:children,...attrs}>
<ReportCard :report="{ title: attrs.title, content: attrs.content }" @click="handleClickReport(content)" />
</template>
</VueMarkdown>
</div>
<div 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 class="message-context" v-show="content.thinking">
<div class="thinking">
<div class="thinking-header flex align-center" @click="toggleThinkingCollapsed">
<span>思考中</span>
<!-- <SvgIcon :name="content.thinkingCollapsed ? 'arrowDown' : 'arrowUp'" size="16" color="#666" /> -->
</div>
<div class="thinking-content" v-show="!content.thinkingCollapsed">
<pre>{{ content.thinkingText }}</pre>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import gsap from 'gsap'
import userThumb from '@/assets/images/user-thumb.jpg'
import agentThumb from '@/assets/images/agent-thumb.png'
import ReportCard from './ReportCard.vue'
import UrlCard from './UrlCard.vue'
import { VueMarkdown } from '@crazydos/vue-markdown'
import type { CustomAttrs } from '@crazydos/vue-markdown'
import rehypeRaw from 'rehype-raw'
const { t } = useI18n()
const props = defineProps<{
content: Object
isLast: Boolean
}>()
const emit = defineEmits(['regenerate'])
const imageList = computed(() => {
const { imageUrls } = props.content
const list = []
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 {}
}
}
}
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 toggleThinkingCollapsed = () => {
props.content.thinkingCollapsed = !props.content.thinkingCollapsed
}
const handleClickReport = (data) => {
// 点击显示报告
}
const handleClickUrls = (data) => {
// 点击显示来源
}
</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%;
}
}
.operate {
margin-top: 1.3rem;
column-gap: 1.2rem;
}
}
.img-list {
column-gap: 1rem;
margin-bottom: 1.4rem;
.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 {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
background-color: #f9f9f9;
.thinking-header {
cursor: pointer;
font-weight: bold;
justify-content: space-between;
}
.thinking-content {
margin-top: 0.5rem;
pre {
white-space: pre-wrap;
font-family: inherit;
font-size: 1.2rem;
color: #666;
}
}
}
</style>
<style lang="less">
.message-txt {
user-select: text;
ul {
list-style-position: inside;
}
code {
white-space: pre-wrap;
}
img {
max-width: 100%;
}
}
</style>