增加中断,重新回复,反馈功能

main
xushilin 4 months ago
parent 3ca236b933
commit bcabae8067

@ -1,7 +1,7 @@
import request from '@/request';
// 获取汇总
// 获取回复
export const getAIResponse = (data) => request({
method : 'POST',
url: '/api/chat',

@ -1,24 +1,11 @@
<template>
<view class="chat">
<view
v-for="m in messages"
:key="m.id"
:id="'msg-' + m.id"
:class="['msg', m.role]"
>
<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'">{{ m.content }}</text>
<view
class="text-voice"
v-if="m.inputType === 'voice'"
@tap="playVoice(m)"
>
<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>
<image class="voice-play" src="@/static/voice-play.png" mode="widthFix"></image>
</view>
</view>
<view v-else class="bubble ai-bubble">
@ -37,23 +24,134 @@
}}</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>
<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>
</template>
<script>
export default {
export default {
props: {
messages: {
type: Array,
default() {
default () {
return [];
},
},
isReplying: {
type: Boolean,
default: false
}
},
data() {
return {
upvoteImage: require('@/static/upvote.png'),
upvoteHighLightImage: require('@/static/upvote-highlight.png'),
isHighLight: false,
upvoteIndex: null,
quickAskList : [
{
id : 1,
label : '虚假信息'
},
{
id : 2,
label : '没有帮助'
},
{
id : 3,
label : '其他'
}
],
askActive : null
}
},
methods: {
changeShow(e) {
this.$emit('changeShow',e.show)
},
selectAsk(id){
this.askActive = id;
},
continueCreate() {
this.$emit('continueCreate')
},
refresh() {
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);
console.log('voicePath', voicePath);
// if (!voicePath) {
// uni.showToast({
// title: "",
@ -78,88 +176,123 @@ export default {
// this.innerAudioContext.play();
},
},
};
};
</script>
<style scoped>
.chat {
<style scoped lang="scss">
.chat {
margin: 6px 0 12px;
}
}
.msg {
.msg {
/* margin: 10px 0; */
display: flex;
margin-bottom: 10px;
padding-bottom: 10px;
}
flex-wrap: wrap;
}
.msg.user {
.msg.user {
justify-content: flex-end;
}
}
.bubble {
.bubble {
max-width: 80%;
padding: 10px 12px;
border-radius: 14px;
font-size: 14px;
line-height: 1.5;
}
}
.user-bubble {
.user-bubble {
background: #4e7bff;
color: #fff;
border-bottom-right-radius: 4px;
margin-right: 6px;
}
}
.ai-bubble {
.ai-bubble {
background: #fff;
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;
}
}
.listen-btn {
.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 {
.ai-card-title {
color: #5f6fff;
font-weight: 600;
margin-bottom: 6px;
}
}
.ai-card-body {
.ai-card-body {
color: #666;
}
}
/* loading animation */
.ai-loading {
/* loading animation */
.ai-loading {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 0;
}
}
.loading-dot {
.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) {
.loading-dot:nth-child(1) {
animation-delay: -0.32s;
}
}
.loading-dot:nth-child(2) {
.loading-dot:nth-child(2) {
animation-delay: -0.16s;
}
}
@keyframes loading-bounce {
@keyframes loading-bounce {
0%,
80%,
100% {
@ -171,15 +304,78 @@ export default {
transform: scale(1.2);
opacity: 1;
}
}
}
.text-voice {
.text-voice {
display: flex;
align-items: center;
}
}
.voice-play {
.voice-play {
width: 20px;
margin-left: 5px;
}
}
.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;
}
}
</style>

@ -4,7 +4,7 @@
<top @clickLeft="openDrawer"></top>
<scroll-view class="content" :scroll-y="true" show-scrollbar="false" scroll-with-animation ref="scrollView">
<front @onSuggestionTap="onQuickAsk" />
<chat :messages="messages" />
<chat :messages="messages" @continueCreate="continueCreate" :isReplying="isReplying" @refresh="refresh" @changeShow="changeShow"/>
</scroll-view>
<view :style="{height: marginBottom + 'px',backgroundColor : '#fff'}" />
<leftDrawer :historyGroups="historyGroups" ref="popup" @changeShow="changeShow"
@ -26,9 +26,6 @@
import chat from "./chat/index.vue";
import leftDrawer from "./leftDrawer/index.vue";
import search from "./search/index.vue";
import {
recognizeAudio
} from "@/utils/uploadVoice.js";
export default {
components: {
top,
@ -47,12 +44,14 @@
show: false,
marginBottom: 0,
isReplying: false,
breakReplying: false
breakReplying: false,
replyData: {},
isRefresh: false
};
},
mounted() {
this.loadChatHistory();
this.scrollToBottom();
// this.scrollToBottom();
let self = this;
uni.onKeyboardHeightChange((res) => {
uni.pageScrollTo({
@ -73,6 +72,29 @@
this.typewriterTimers = {};
},
methods: {
//
refresh() {
this.messages.splice(this.messages.length - 1, 1);
this.inputText = this.messages[this.messages.length - 1].content;
this.isRefresh = true;
this.onSend();
},
//
continueCreate() {
this.breakReplying = false;
this.isReplying = true;
if (!this.isLoading) {
this.messages[this.messages.length - 1].isBreak = false;
this.replyAction(this.replyData, true)
} else {
this.messages.splice(this.messages.length - 1, 1);
this.messages.push({
id: this.loadingId,
role: "assistant",
loading: true,
});
}
},
//
handleBreak() {
if (this.isLoading) {
@ -84,11 +106,12 @@
id: replyId,
role: "assistant",
type: "text",
content: '已中断回复 ',
displayText: "",
content: '',
displayText: "已停止",
isBreak: true
});
this.isReplying = false;
this.isLoading = false;
this.scrollToBottom();
}
this.breakReplying = true;
},
@ -196,10 +219,11 @@
},
//
async onSend(inputType = "text", inputContent = "", duration = undefined) {
if(this.isReplying) return;
if (this.isReplying) return;
const text = (this.inputText || "").trim();
if (!text || this.isLoading) return;
this.baseId = Date.now();
if (!this.isRefresh) {
// 1.
this.messages.push({
id: this.baseId,
@ -210,7 +234,7 @@
inputContent,
duration,
});
}
// 2. loading
this.loadingId = this.baseId + 0.5;
this.messages.push({
@ -219,19 +243,24 @@
loading: true,
});
this.scrollToBottom();
this.isReplying = true;
this.inputText = "";
this.isReplying = true;
this.isLoading = true;
this.isRefresh = false;
this.addToHistory(text);
// 3. AI
const reply = await getAIResponse({
message: text,
});
if(this.breakReplying) {
this.replyData = reply;
this.isLoading = false;
if (this.breakReplying) {
this.breakReplying = false;
return;
}
this.isLoading = false;
this.replyAction(reply)
},
replyAction(reply, isCreate) {
let content = ''
if (reply.errMsg) {
content = `请求出错: ${reply.errMsg}`
@ -243,6 +272,7 @@
if (loadingIdx > -1) this.messages.splice(loadingIdx, 1);
// 5. +
const replyId = this.baseId + 1;
if (!isCreate) {
this.messages.push({
id: replyId,
role: "assistant",
@ -250,9 +280,10 @@
content,
displayText: "",
});
}
this.$nextTick(() => this.scrollToBottom());
this.typewriter(replyId, content);
},
//
typewriter(messageId, fullText) {
@ -262,7 +293,7 @@
if (this.typewriterTimers[messageId]) {
clearInterval(this.typewriterTimers[messageId]);
}
let index = 0;
let index = msg.displayText.length;
msg.displayText = fullText.substring(0, index + 1);
index += 1;
const speed = 50; // 50ms
@ -271,6 +302,7 @@
if (this.breakReplying) {
clearInterval(timer);
delete this.typewriterTimers[messageId];
msg.isBreak = true;
this.isReplying = false;
this.breakReplying = false
this.isLoading = false;
@ -285,7 +317,10 @@
// 使
msg.displayText = fullText;
this.isReplying = false;
this.breakReplying = false
this.breakReplying = false;
this.$nextTick(() => {
this.scrollToBottom();
})
}
}, speed);
this.typewriterTimers[messageId] = timer;
@ -337,8 +372,4 @@
box-sizing: border-box;
}
/*
::v-deep .drawer-scroll {
padding-top: 44px;
} */
</style>

@ -7,7 +7,7 @@
class="history-popup"
@change="changeShow"
>
<view class="drawer-mask">
<view class="drawer-mask" :style="{paddingTop : statusBarHeight + 'px'}">
<view class="drawer">
<scroll-view class="drawer-scroll" scroll-y show-scrollbar="false">
<view v-for="g in historyGroups" :key="g.date" class="drawer-group">
@ -26,7 +26,7 @@
</scroll-view>
<view class="drawer-footer">
<view class="user-icon">👤</view>
<text class="user-name">用户<EFBFBD> </text>
<text class="user-name">用户</text>
<view class="footer-gear" @tap="clearAllHistory"></view>
</view>
</view>
@ -45,6 +45,14 @@ export default {
}
}
},
data () {
return {
statusBarHeight : 0
}
},
async mounted(){
this.statusBarHeight = uni.getSystemInfoSync().statusBarHeight / 2;
},
methods : {
onHistoryItemTap(text){
this.$emit('onHistoryItemTap',text)
@ -157,4 +165,8 @@ export default {
width: 24px;
text-align: center;
}
// ::v-deep .drawer-scroll {
// }
</style>

@ -145,6 +145,7 @@
}
uni.showLoading({
title: "识别中...",
mask : true
});
const text = await recognizeAudio(res.tempFilePath);
if (!text?.trim()) {

@ -46,18 +46,17 @@ const request = (config) => {
})
.catch((error) => {
let { message } = error;
if (message === "Network Error") {
message = "网络不稳定,请检查网络 ";
} else if (error.errMsg === "request:fail") {
message = "后端接口连接异常";
} else if (error.errMsg === "request:fail timeout") {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
} else {
message = `抱歉,出了点问题: ${error.errMsg || error.message}`;
}
// if (message === "Network Error") {
// message = "网络不稳定,请检查网络 ";
// } else if (error.errMsg === "request:fail") {
// message = "后端接口连接异常";
// } else if (error.errMsg === "request:fail timeout") {
// message = "系统接口请求超时";
// } else if (message.includes("Request failed with status code")) {
// message = "系统接口" + message.substr(message.length - 3) + "异常";
// } else {
// message = `抱歉,出了点问题: ${error.errMsg || error.message}`;
// }
resolve(error);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -10,7 +10,7 @@ export const recognizeAudio = async (tempFilePath) => {
});
const uploadRes = await new Promise((resolve, reject) => {
uni.uploadFile({
url: `http://192.168.1.18:9022/recognize_speech`,
url: `http://192.168.1.25:9022/recognize_speech`,
filePath: tempFilePath,
name: "speech",
formData: {

Loading…
Cancel
Save