增加文本工具,增加添加、删除快捷提问功能

main
xushilin 4 months ago
parent bcabae8067
commit 9fba96e374

@ -5,7 +5,8 @@ if (process.env.NODE_ENV === 'production') {
baseUrl = 'http://106.227.91.181:9081/api';
} else {
// 非生产环境代码
baseUrl = 'http://192.168.1.25:9020';
// baseUrl = 'http://192.168.1.12:9020';
baseUrl = 'http://106.227.91.181:9020'
}
const config = {
baseUrl

@ -2,7 +2,7 @@
<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'">{{ m.content }}</text>
<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>
@ -42,15 +42,43 @@
</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>
<uni-popup ref="popup" type="bottom" class="popup" @change="changeShow">
<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 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 :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>
@ -62,6 +90,7 @@
</template>
<script>
import {copyText} from '@/utils/utils.js'
export default {
props: {
messages: {
@ -72,43 +101,103 @@
},
isReplying: {
type: Boolean,
default: false
default: false,
textTool: []
}
},
data() {
return {
upvoteImage: require('@/static/upvote.png'),
upvoteHighLightImage: require('@/static/upvote-highlight.png'),
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 : '虚假信息'
quickAskList: [{
id: 1,
label: '数据不准确'
},
{
id : 2,
label : '没有帮助'
id: 2,
label: '没有帮助'
},
{
id : 3,
label : '其他'
id: 3,
label: '其他'
}
],
askActive : null
askActive: null,
textToolStyle : {},
isOpenTextTool : false,
showTool: false,
screenWidth : 0,
selectText : ''
}
},
mounted () {
this.screenWidth = uni.getSystemInfoSync().screenWidth;
},
methods: {
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)
this.$emit('changeShow', e.show)
},
selectAsk(id){
selectAsk(id) {
this.askActive = id;
},
continueCreate() {
this.$emit('continueCreate')
},
refresh() {
this.isHighLight = false;
this.upvoteIndex = null;
this.$emit('refresh')
},
upvote() {
@ -132,12 +221,12 @@
}
if (!this.isHighLight) {
this.$refs.popup.open();
}else{
} else {
this.isHighLight = !this.isHighLight;
}
this.upvoteIndex = 1;
},
submitFeedback(){
submitFeedback() {
this.$refs.popup.close();
this.isHighLight = true;
uni.showToast({
@ -146,7 +235,7 @@
duration: 1500
})
},
closeFeedback(){
closeFeedback() {
this.$refs.popup.close();
this.isHighLight = false;
},
@ -182,6 +271,7 @@
<style scoped lang="scss">
.chat {
margin: 6px 0 12px;
position: relative;
}
.msg {
@ -316,6 +406,58 @@
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;
}
@ -325,6 +467,7 @@
padding: 0 20px;
border-radius: 10px 10px 0 0;
padding-bottom: 20px;
.top {
position: relative;
text-align: center;
@ -349,18 +492,19 @@
font-size: 18px;
}
}
.quick-ask{
.quick-ask {
display: flex;
.ask{
.ask {
padding: 5px 15px;
border: 1px solid #ddd;
border-radius: 10px;
margin-right: 10px;
font-size: 14px;
}
.active{
.active {
background-color: #007AFF;
color: #fff;
}
@ -378,4 +522,16 @@
font-size: 14px;
}
}
.mark-layer{
position: fixed;
width: 100vw;
height: 100vh;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 9999;
opacity: 0;
}
</style>

@ -4,7 +4,8 @@
<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" @continueCreate="continueCreate" :isReplying="isReplying" @refresh="refresh" @changeShow="changeShow"/>
<chat :messages="messages" @continueCreate="continueCreate" :isReplying="isReplying" @refresh="refresh"
@changeShow="changeShow" @changeInputText="changeInputText"/>
</scroll-view>
<view :style="{height: marginBottom + 'px',backgroundColor : '#fff'}" />
<leftDrawer :historyGroups="historyGroups" ref="popup" @changeShow="changeShow"
@ -49,7 +50,7 @@
isRefresh: false
};
},
mounted() {
async mounted() {
this.loadChatHistory();
// this.scrollToBottom();
let self = this;
@ -60,9 +61,12 @@
class: ".content",
});
});
// #ifdef APP-PLUS
this.$nextTick(() => {
this.marginBottom = this.$refs.searchRef.getHeight() || 103;
});
// #endif
this.marginBottom = 103
},
beforeDestroy() {
//
@ -242,11 +246,11 @@
role: "assistant",
loading: true,
});
this.scrollToBottom();
this.inputText = "";
this.isReplying = true;
this.isLoading = true;
this.isRefresh = false;
this.scrollToBottom();
this.addToHistory(text);
// 3. AI
const reply = await getAIResponse({
@ -371,5 +375,4 @@
width: 100%;
box-sizing: border-box;
}
</style>

@ -1,19 +1,27 @@
<template>
<view>
<view class="dock">
<scroll-view class="quick-actions horizontal" scroll-x show-scrollbar="false">
<view class="qa-btn minor" @tap="onSwitchModel"></view>
<view class="qa-btn" @tap="onQuickAsk(item.quickAskText)" v-for="item in quickAskList" :key="item.id">
{{ item.label }}
<view style="display: flex;">
<scroll-view class="quick-actions horizontal" scroll-x show-scrollbar="false"
style="width: calc(100% - 50px);">
<!-- <view class="qa-btn minor" @tap="onSwitchModel"></view> -->
<view class="qa-btn" @tap="onQuickAsk(item.quickAskText)" v-for="item in quickAskList"
:key="item.quickAskText" @longpress.prevent="deleteQucikAsk(item)">
{{ item.quickAskText }}
</view>
</scroll-view>
<view class="quick-add" @tap="inputDialogToggle">
<image style="width: 40px;" src="@/static/plus-circle-fill.png" mode="widthFix"></image>
</view>
</scroll-view>
</view>
<view class="input-bar">
<input class="input" confirm-type="send" v-model="inputTextValue" @confirm="onSend()"
placeholder="你可以说…" placeholder-class="ph" />
<view :class="['mic', { recording: isRecording }]" @touchstart.stop="onPressMic"
@touchmove.stop="onMoveMic" @touchend.stop="onReleaseMic">🎙</view>
<!-- <button class="send" type="primary" @tap="onSend"></button> -->
<view :class="['send', (!isReplying && inputTextValue) ? 'normal' : 'disabled']">
<view
:class="['send', (!isReplying && inputTextValue && inputTextValue.trim()) ? 'normal' : 'disabled']">
<image v-if="isReplying" src="@/static/break.png" mode="widthFix" style="width: 20px;"
@tap="handleBreak"></image>
<image v-else src="@/static/top-arrows.png" mode="widthFix" style="width: 20px;" @tap="onSend">
@ -30,7 +38,12 @@
</view>
</view>
<view v-if="isRecording" class="mask-layer" @touchmove.stop.prevent> </view>
<uni-popup ref="inputDialog" type="dialog" style="z-index: 10003;" >
<uni-popup-dialog ref="inputClose" mode="input" title="添加快捷提问" v-model="dialogText" placeholder="请输入内容"
@confirm="dialogInputConfirm" :maxlength="15" ></uni-popup-dialog>
</uni-popup>
<view v-if="isRecording" class="mask-layer"> </view>
</view>
</template>
@ -51,25 +64,12 @@
},
data() {
return {
quickAskList: [{
id: 1,
label: "自我介绍",
quickAskText: "你是谁?",
},
{
id: 2,
label: "快捷提问",
quickAskText: "今日任务有哪些?",
},
quickAskList: [
{
id: 3,
label: "快捷提问",
quickAskText: "展示一份报表示例",
quickAskText : '设备运行情况'
},
{
id: 4,
label: "快捷提问",
quickAskText: "生成日报模版",
quickAskText : '今日出入库数据'
}
],
searchHeight: 0,
@ -79,7 +79,8 @@
recorder: null,
recordStartY: 0,
recordStartTs: 0,
recordSimTimer: null
recordSimTimer: null,
dialogText: ''
}
},
mounted() {
@ -102,6 +103,45 @@
}
},
methods: {
deleteQucikAsk(item){
uni.showModal({
title: "提示",
content: "确定要删除这条快捷提问?",
success : res => {
if(res.confirm){
this.quickAskList = this.quickAskList.filter(ele => ele.quickAskText !== item.quickAskText);
uni.showToast({
title: '删除成功',
icon: 'none'
})
}
}
})
},
inputDialogToggle() {
this.dialogText = '';
this.$refs.inputDialog.open()
},
dialogInputConfirm() {
if(!this.dialogText && !this.dialogText.trim()){
uni.showToast({
title: '内容不能为空',
icon: 'none'
})
return;
}
let index = this.quickAskList.findIndex(item => item.quickAskText.trim() === this.dialogText.trim());
if(index > -1){
uni.showToast({
title: '不能重复添加内容',
icon: 'none'
})
return;
}
this.quickAskList.unshift({
quickAskText : this.dialogText,
});
},
handleBreak() {
this.$emit('handleBreak')
},
@ -145,7 +185,7 @@
}
uni.showLoading({
title: "识别中...",
mask : true
mask: true
});
const text = await recognizeAudio(res.tempFilePath);
if (!text?.trim()) {
@ -249,6 +289,7 @@
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
z-index: 999;
}
.quick-actions {
@ -257,7 +298,15 @@
.quick-actions.horizontal {
white-space: nowrap;
width: 95%;
width: 100%;
padding-right: 10px;
box-sizing: border-box;
}
.quick-add {
display: flex;
align-items: center;
justify-content: center;
}
.qa-btn {

@ -2,7 +2,7 @@ import config from "@/config";
// import errorCode from "@/utils/errorCode";
import { toast, showConfirm, tansParams } from "@/utils/common";
let timeout = 10000;
let timeout = 30000;
const baseUrl = config.baseUrl;
const request = (config) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -32,4 +32,8 @@
display: flex;align-items: center;
}
::v-deep uni-toast, uni-modal{
z-index: 100001;
}

@ -1,6 +1,5 @@
export default {
state : {
//#ifdef APP-PLUS
// microphoneAuthorized : uni.getAppAuthorizeSetting().microphoneAuthorized
},
mutations : {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,21 +1,39 @@
export function formatDate(date, fmt) {
const o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'h+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
S: date.getMilliseconds() // 毫秒
};
if (/(y+)/.test(fmt)) {
// eslint-disable-next-line no-param-reassign
fmt = fmt.replace(RegExp.$1, `${date.getFullYear()}`.substr(4 - RegExp.$1.length));
}
for (const k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
// eslint-disable-next-line no-param-reassign
fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : `00${o[k]}`.substr(`${o[k]}`.length));
}
}
return fmt;
const o = {
'M+': date.getMonth() + 1, // 月份
'd+': date.getDate(), // 日
'h+': date.getHours(), // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
S: date.getMilliseconds() // 毫秒
};
if (/(y+)/.test(fmt)) {
// eslint-disable-next-line no-param-reassign
fmt = fmt.replace(RegExp.$1, `${date.getFullYear()}`.substr(4 - RegExp.$1.length));
}
for (const k in o) {
if (new RegExp(`(${k})`).test(fmt)) {
// eslint-disable-next-line no-param-reassign
fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : `00${o[k]}`.substr(`${o[k]}`.length));
}
}
return fmt;
}
export function copyText(text) {
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({
title: '复制成功',
icon: 'none'
});
},
fail: (err) => {
uni.showToast({
title: '复制失败',
icon: 'none'
});
}
});
}
Loading…
Cancel
Save