You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

581 lines
12 KiB
Vue

4 months ago
<template>
<view class="chat">
<view v-for="(m,index) in messages" :key="m.id" :id="'msg-' + m.id" :class="['msg', m.role]">
<view v-if="m.role === 'user'" class="bubble user-bubble">
<text v-if="m.inputType === 'text'" @longpress.prevent="loadTool($event,m)">{{ m.content }}</text>
<view class="text-voice" v-if="m.inputType === 'voice'" @tap="playVoice(m)">
<text>{{ m.duration }}</text>
<image class="voice-play" src="@/static/voice-play.png" mode="widthFix"></image>
</view>
</view>
3 months ago
<view v-if="m.role !== 'user' && m.type !== 'card' && !m.loading && m.src " class="ai-voice" style="width: 100%;">
<view class="ai-voice-play" @tap="clickAiVocie(m.src)">
<image class="voice-play" :src="leftVoiceImgList[current].image" mode="widthFix"
v-if="playSrc === m.src">
</image>
<image class="voice-play" :src="leftVoiceImgList[2].image" mode="widthFix"
v-else>
</image>
<text
style="margin-left: 5px;font-size: 14px;">{{ m.duration ? Math.ceil( m.duration) : 0 }}"</text>
</view>
</view>
<view v-if="m.role !== 'user'" class="bubble ai-bubble">
<view v-if="m.type === 'card'" class="ai-card">
<view class="ai-card-title">{{ m.title }}</view>
<view class="ai-card-body">{{ m.content }}</view>
</view>
<view v-else-if="m.loading" class="ai-loading">
<view class="loading-dot"></view>
<view class="loading-dot"></view>
<view class="loading-dot"></view>
</view>
<view v-else>
<text>{{
4 months ago
m.displayText !== undefined ? m.displayText : m.content
}}</text>
</view>
</view>
<view class="continue-create" v-if="m.isBreak && index === messages.length - 1">
<view class="text" @tap="continueCreate">
继续生成
</view>
<!-- upvote-highlight -->
</view>
<view class="tool-box" v-if="!isReplying && index === messages.length - 1 && m.role === 'assistant'">
<image class="tool-image" src="@/static/refresh.png" mode="widthFix" @tap="refresh"></image>
<image class="tool-image"
:src="isHighLight ? (upvoteIndex === 0 ? upvoteHighLightImage : upvoteImage) : upvoteImage"
mode="widthFix" @tap="upvote"></image>
<image class="tool-image rote"
:src="isHighLight ? (upvoteIndex === 1 ? upvoteHighLightImage : upvoteImage) : upvoteImage"
mode="widthFix" @tap="unUpvote"></image>
</view>
</view>
<view class="text-tool" :class="{'show': showTool}" :style="textToolStyle" v-if="isOpenTextTool">
<view class="tool-item" v-for="(item, index) in textToolList" :key="item.id"
:style="{animationDelay: index * 0.05 + 's'}" @tap="selectTextTool(item.id)">
<image class="img" :src="item.imageUrl" mode="widthFix"></image>
<text class="text">{{item.text}}</text>
</view>
</view>
<view class="mark-layer" v-if="isOpenTextTool" @touchstart="closeTool"></view>
<uni-popup ref="popup" type="bottom" class="popup" @change="changeShow">
<view class="feedback">
<view class="top">
<view class="title">反馈</view>
<view class="close" @tap="closeFeedback">×</view>
</view>
<view class="quick-ask">
<view :class="['ask',item.id === askActive ? 'active' : '']" v-for="item in quickAskList"
:key="item.id" @tap="selectAsk(item.id)">{{item.label}}</view>
</view>
<view>
<textarea class="textarea" placeholder="我们想知道你对此回答不满意的原因,你认为更好的回答是什么?"></textarea>
</view>
<button type="primary" style="font-size: 16px;" @tap="submitFeedback"></button>
</view>
</uni-popup>
</view>
4 months ago
</template>
<script>
import {
copyText
} from '@/utils/utils.js'
export default {
props: {
messages: {
type: Array,
default () {
return [];
},
},
isReplying: {
type: Boolean,
default: false,
},
isPlayingVoice: {
type: Boolean,
default: false,
},
playSrc : {
type: String,
default: '',
},
},
data() {
return {
upvoteImage: require('@/static/upvote.png'),
upvoteHighLightImage: require('@/static/upvote-highlight.png'),
leftVoiceImgList: [{
image: require('@/static/voice-play-left1.png')
}, {
image: require('@/static/voice-play-left2.png')
}, {
image: require('@/static/voice-play-left3.png')
}],
current: 2,
textToolList: [{
id: 1,
text: '复制',
imageUrl: require('@/static/copy.png')
}, {
id: 2,
text: '修改',
imageUrl: require('@/static/edit.png')
}],
isHighLight: false,
upvoteIndex: null,
quickAskList: [{
id: 1,
label: '数据不准确'
},
{
id: 2,
label: '没有帮助'
},
{
id: 3,
label: '其他'
}
],
askActive: null,
textToolStyle: {},
isOpenTextTool: false,
showTool: false,
screenWidth: 0,
selectText: '',
voiceTimer: null,
}
},
mounted() {
this.screenWidth = uni.getSystemInfoSync().screenWidth;
},
watch: {
isPlayingVoice(val) {
if (val) {
this.voiceTimer = setInterval(() => {
if (this.current === 2) {
this.current = -1;
}
this.current += 1;
3 months ago
}, 300)
}else{
if (this.voiceTimer) {
clearInterval(this.voiceTimer)
};
this.current = 2;
}
},
},
methods: {
clickAiVocie(src) {
this.$emit('handleVoice', src)
},
selectTextTool(id) {
switch (id) {
case 1:
copyText(this.selectText)
break;
case 2:
this.$emit('changeInputText', this.selectText)
default:
break;
}
this.closeTool();
},
closeTool() {
this.showTool = false;
this.isOpenTextTool = false;
this.$emit('changeShow', false)
},
loadTool($event, m) {
this.selectText = m.content;
uni.createSelectorQuery().select(`#msg-${m.id}`).boundingClientRect((rect) => {
let height = rect.height || 0;
if ($event.touches[0].pageX > (this.screenWidth / 2)) {
this.textToolStyle = {
top: $event.target.offsetTop + height - 10 + 'px',
right: this.screenWidth - Math.ceil($event.touches[0].pageX) + 'px'
}
} else {
this.textToolStyle = {
top: $event.target.offsetTop + height - 10 + 'px',
left: Math.ceil($event.touches[0].pageX) + 'px'
}
}
this.isOpenTextTool = true;
this.$emit('changeShow', true);
// 确保DOM更新后再触发动画
this.$nextTick(() => {
this.showTool = true;
});
}).exec();
},
changeShow(e) {
this.$emit('changeShow', e.show)
},
selectAsk(id) {
this.askActive = id;
},
continueCreate() {
this.$emit('continueCreate')
},
refresh() {
this.isHighLight = false;
this.upvoteIndex = null;
this.$emit('refresh')
},
upvote() {
if (this.upvoteIndex !== 0) {
this.isHighLight = true
} else {
this.isHighLight = !this.isHighLight;
}
this.upvoteIndex = 0;
if (this.isHighLight) {
uni.showToast({
title: '反馈成功',
icon: 'none',
duration: 1500
})
}
},
unUpvote() {
if (this.upvoteIndex !== 1) {
this.$refs.popup.open();
}
if (!this.isHighLight) {
this.$refs.popup.open();
} else {
this.isHighLight = !this.isHighLight;
}
this.upvoteIndex = 1;
},
submitFeedback() {
this.$refs.popup.close();
this.isHighLight = true;
uni.showToast({
title: '反馈成功',
icon: 'none',
duration: 1500
})
},
closeFeedback() {
this.$refs.popup.close();
this.isHighLight = false;
},
playVoice(voicePath) {
console.log('voicePath', voicePath);
// if (!voicePath) {
// uni.showToast({
// title: "无可播放的语音",
// icon: "none",
// });
// return;
// }
// if (!this.innerAudioContext) {
// this.innerAudioContext = uni.createInnerAudioContext();
// this.innerAudioContext.autoplay = false;
// this.innerAudioContext.onError(() => {
// uni.showToast({
// title: "播放失败",
// icon: "none",
// });
// });
// }
// try {
// this.innerAudioContext.stop();
// } catch (e) {}
// this.innerAudioContext.src = voicePath;
// this.innerAudioContext.play();
},
},
};
4 months ago
</script>
<style scoped lang="scss">
.chat {
margin: 6px 0 12px;
position: relative;
}
.msg {
/* margin: 10px 0; */
display: flex;
margin-bottom: 10px;
padding-bottom: 10px;
flex-wrap: wrap;
}
.msg.user {
justify-content: flex-end;
}
.bubble {
max-width: 80%;
padding: 10px 12px;
border-radius: 14px;
line-height: 1.5;
}
.user-bubble {
background: #4e7bff;
color: #fff;
border-bottom-right-radius: 4px;
margin-right: 6px;
}
.ai-bubble {
3 months ago
background: #F3F7F9;
color: #333;
border-bottom-left-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.continue-create {
width: 100%;
.text {
font-size: 12px;
border: 1px solid #ddd;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
width: 90px;
padding: 5px 0;
margin-top: 8px;
}
}
.tool-box {
width: 100%;
margin-top: 15px;
padding-left: 5px;
display: flex;
align-items: center;
.tool-image {
width: 14px;
margin-right: 15px;
}
.rote {
transform: rotate(180deg);
}
}
.listen-btn {
margin-left: 8px;
color: #6b7280;
font-size: 14px;
}
.ai-card-title {
color: #5f6fff;
font-weight: 600;
margin-bottom: 6px;
}
.ai-card-body {
color: #666;
}
/* loading animation */
.ai-loading {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 0;
}
.loading-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #9ca3af;
animation: loading-bounce 1.5s ease-in-out infinite both;
}
.loading-dot:nth-child(1) {
animation-delay: -0.32s;
}
.loading-dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes loading-bounce {
0%,
80%,
100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1.2);
opacity: 1;
}
}
.text-voice {
display: flex;
align-items: center;
}
.voice-play {
width: 20px;
margin-left: 5px;
}
.text-tool {
position: absolute;
background-color: #fff;
z-index: 10000;
color: #000;
border-radius: 5px;
box-shadow: 0 0 1px 1px #e4e4e4;
opacity: 0;
transform: translateY(-10px) scale(0.9);
transition: opacity 0.3s ease, transform 0.3s ease;
&.show {
opacity: 1;
transform: translateY(0) scale(1);
}
.tool-item {
display: flex;
width: 160px;
padding: 10px;
box-sizing: border-box;
align-items: center;
border-bottom: 1px solid #ddd;
font-size: 14px;
opacity: 0;
transform: translateX(-10px);
animation: slideInItem 0.3s ease forwards;
}
.tool-item:last-child {
border-bottom: 0px;
}
.img {
width: 16px;
margin-right: 10px;
}
}
@keyframes slideInItem {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.popup {
z-index: 99999;
}
.feedback {
background-color: #fff;
padding: 0 20px;
border-radius: 10px 10px 0 0;
padding-bottom: 20px;
.top {
position: relative;
text-align: center;
padding: 10px 0;
.title {
font-weight: bold;
}
.close {
background-color: #ddd;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
}
}
.quick-ask {
display: flex;
.ask {
padding: 5px 15px;
border: 1px solid #ddd;
border-radius: 10px;
margin-right: 10px;
font-size: 14px;
}
.active {
background-color: #007AFF;
color: #fff;
}
}
.textarea {
width: 100%;
background-color: rgba(45, 44, 46, .05);
// min-height: 60px;
padding: 10px 12px;
box-sizing: border-box;
margin-bottom: 20px;
margin-top: 10px;
border-radius: 10px;
font-size: 14px;
}
}
.mark-layer {
position: fixed;
width: 100vw;
height: 100vh;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 9999;
opacity: 0;
}
.ai-voice-play {
width: 60px;
3 months ago
background-color: #F3F7F9;
margin-bottom: 10px;
padding: 10px 12px;
border-radius: 14px;
display: flex;
align-items: center;
}
</style>