1
This commit is contained in:
292
pages/chat/index.js
Normal file
292
pages/chat/index.js
Normal file
@@ -0,0 +1,292 @@
|
||||
import request, { streamRequest } from "@/utils/request";
|
||||
import { copyText } from "@/utils/common"
|
||||
let outTimer; //输出定时器
|
||||
let outStrList = [];//输出字符串数组
|
||||
const app = getApp()
|
||||
|
||||
let chat_uuid
|
||||
//工具
|
||||
let toolText = ''
|
||||
let toolInfo = {}
|
||||
Page({
|
||||
data: {
|
||||
chatList: [], //会话记录
|
||||
aiStatus: 3,//1表示接口响应中,2表示接口响应完毕,3表示完全输出完毕
|
||||
keyboardBottom: 0, //底部键盘高度
|
||||
scrollTop: 0, //滚动条位置
|
||||
inputText: '',//输入框内容
|
||||
},
|
||||
onLoad() {
|
||||
let str = `您好!我是术极守护AI助手 🤖\n\n我可以为您解答术后康复相关的问题。请注意,我只能提供基于康复指南的一般性建议,如遇紧急情况请立即就医或联系您的主治医生。`
|
||||
let mdResult = app.towxml(str, 'markdown', {
|
||||
base: "www.xxx.com",
|
||||
});
|
||||
let list = [
|
||||
{
|
||||
loading: false,
|
||||
end: true,
|
||||
chat_type: 2,
|
||||
md_content: mdResult,
|
||||
chat_content: str,
|
||||
quick_btn: [
|
||||
{ text: '🩹伤口有轻微渗液怎么办?' },
|
||||
{ text: '🌡️术后轻微发烧正常吗?' },
|
||||
{ text: '🚿什么时候可以洗澡?' },
|
||||
{ text: '🥗饮食需要注意什么?' }
|
||||
]
|
||||
}
|
||||
]
|
||||
this.setData({
|
||||
chatList: list
|
||||
})
|
||||
},
|
||||
onShow() {
|
||||
this.getTabBar((tabBar) => {
|
||||
tabBar.setData({
|
||||
selected: 'AI助手',
|
||||
})
|
||||
})
|
||||
},
|
||||
//监听键盘弹出
|
||||
bindkeyboardheightchange(event) {
|
||||
let height = event.detail.height
|
||||
let { keyboardBottom } = this.data
|
||||
let scrollTop = 0
|
||||
//获取当前滚动距离
|
||||
const query = wx.createSelectorQuery().in(this)
|
||||
query.select(".silder").scrollOffset()
|
||||
query.exec(function (res) {
|
||||
scrollTop = res[0].scrollTop
|
||||
})
|
||||
this.setData({
|
||||
keyboardBottom: height > 0 ? height - 50 : keyboardBottom,
|
||||
}, () => {
|
||||
if (height > 0) {
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
scrollTop: scrollTop + height
|
||||
})
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
},
|
||||
//键盘失去焦点
|
||||
bindkeyboardBlur() {
|
||||
let { scrollTop, keyboardBottom } = this.data
|
||||
let endScrollTop = scrollTop - keyboardBottom
|
||||
this.setData({
|
||||
keyboardBottom: 0
|
||||
}, () => {
|
||||
this.setData({
|
||||
scrollTop: endScrollTop
|
||||
})
|
||||
})
|
||||
},
|
||||
//监听输入框内容
|
||||
changeInput(e) {
|
||||
let value = e.detail.value
|
||||
this.setData({
|
||||
inputText: value
|
||||
})
|
||||
},
|
||||
async handQuick(e) {
|
||||
let { data } = e.currentTarget.dataset
|
||||
this.pushUserTemplate(data.text)
|
||||
this.sendMessage(data.text)
|
||||
|
||||
},
|
||||
|
||||
// 文字提交发送
|
||||
submitInput() {
|
||||
let { inputText, aiStatus } = this.data
|
||||
if (!inputText.trim() == '' && aiStatus) {
|
||||
console.log('111--');
|
||||
this.pushUserTemplate(inputText)
|
||||
this.sendMessage(inputText)
|
||||
}
|
||||
},
|
||||
//发送聊天
|
||||
async sendMessage(text) {
|
||||
let { chatList, aiStatus } = this.data
|
||||
if (aiStatus == 3) {
|
||||
let that = this
|
||||
//初始化变量
|
||||
toolText = ''
|
||||
toolInfo = {}
|
||||
chat_uuid = String(new Date().getTime())
|
||||
this.setData({
|
||||
aiStatus: 1
|
||||
})
|
||||
//输出
|
||||
this.timerOutput()
|
||||
//请求
|
||||
streamRequest("/ai/chat", {
|
||||
message: text,
|
||||
chat_uuid: chat_uuid,
|
||||
}, this.onChunkReceived).then(() => {
|
||||
console.log('-------------成功-----------');
|
||||
if (this.data.aiStatus != 3) {
|
||||
that.setData({
|
||||
aiStatus: 2
|
||||
})
|
||||
}
|
||||
}).catch(() => {
|
||||
console.log("------失败1---------");
|
||||
that.data.chatList.at(-1).loading = false
|
||||
that.setData({
|
||||
aiStatus: 3,
|
||||
chatList: chatList
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
//填充用户的聊天数据
|
||||
pushUserTemplate(text) {
|
||||
let { chatList } = this.data
|
||||
let userItem = {
|
||||
loading: false,
|
||||
chat_type: 1,
|
||||
con_type: 1,
|
||||
chat_content: text,
|
||||
md_content: '',
|
||||
}
|
||||
let aiItem = {
|
||||
chat_type: 2,
|
||||
chat_content: '',
|
||||
md_content: '',
|
||||
con_type: 1,
|
||||
loading: true,
|
||||
}
|
||||
chatList.push(userItem)
|
||||
|
||||
chatList.push(aiItem)
|
||||
|
||||
//要修改的数据
|
||||
let setData = {
|
||||
chatList,
|
||||
scrollTop: 80000,
|
||||
inputText: ''
|
||||
}
|
||||
this.setData(setData, () => {
|
||||
this.setData({
|
||||
scrollTop: 80000
|
||||
})
|
||||
})
|
||||
},
|
||||
//只填充ai的默认回复
|
||||
pushAiTemplate(text, options = {}) {
|
||||
let { chatList } = this.data
|
||||
let mdResult = app.towxml(text, 'markdown', {
|
||||
base: "www.xxx.com",
|
||||
});
|
||||
let aiItem = {
|
||||
chat_type: 2,
|
||||
chat_content: text,
|
||||
md_content: mdResult,
|
||||
con_type: 1,
|
||||
quick_btn: options.quick_btn || [],
|
||||
...options
|
||||
}
|
||||
chatList.push(aiItem)
|
||||
//要修改的数据
|
||||
let setData = {
|
||||
chatList,
|
||||
scrollTop: 80000,
|
||||
inputText: ''
|
||||
}
|
||||
this.setData(setData, () => {
|
||||
this.setData({
|
||||
scrollTop: 80000
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
//定时器输出
|
||||
timerOutput() {
|
||||
let { chatList } = this.data
|
||||
let that = this
|
||||
clearInterval(outTimer)
|
||||
outStrList = []
|
||||
let lastChat = this.data.chatList.at(-1)
|
||||
//定时
|
||||
outTimer = setInterval(async () => {
|
||||
if ((outStrList.length == 0) && that.data.aiStatus == 2) {
|
||||
clearInterval(outTimer)
|
||||
lastChat.end = true
|
||||
//如果有工具,设置工具对象
|
||||
let text = lastChat.chat_content
|
||||
// 上传AI响应结果
|
||||
if (text) {
|
||||
await request.post("/ai/history/upload", {
|
||||
chat_content: text,
|
||||
chat_uuid: chat_uuid,
|
||||
})
|
||||
}
|
||||
//调用工具
|
||||
if (toolText) { }
|
||||
that.setData({
|
||||
chatList,
|
||||
aiStatus: 3
|
||||
})
|
||||
|
||||
} else if (outStrList.length > 0) {
|
||||
let firstValue = outStrList.shift();
|
||||
lastChat.chat_content += firstValue
|
||||
//转换md
|
||||
let mdResult = app.towxml(lastChat.chat_content, 'markdown', {
|
||||
base: "www.xxx.com",
|
||||
});
|
||||
lastChat.md_content = mdResult
|
||||
lastChat.loading = false
|
||||
that.setData({
|
||||
chatList: chatList,
|
||||
scrollTop: 80000
|
||||
})
|
||||
}
|
||||
}, 50)
|
||||
},
|
||||
//流回调
|
||||
onChunkReceived(data) {
|
||||
data.forEach((item) => {
|
||||
let value = item?.choices[0].delta.content ?? ''
|
||||
let call = item?.choices[0].delta.tool_calls
|
||||
//储存tool的id和名称
|
||||
if (call) {
|
||||
if (call[0].id) {
|
||||
toolInfo.tool_call_id = call[0].id
|
||||
toolInfo.name = call[0].function.name
|
||||
}
|
||||
let tool = call[0].function.arguments ?? ''
|
||||
toolText += tool
|
||||
}
|
||||
if (value) {
|
||||
outStrList.push(value)
|
||||
}
|
||||
})
|
||||
},
|
||||
//停止
|
||||
async stopMessage() {
|
||||
clearInterval(outTimer)
|
||||
let lastChat = this.data.chatList.at(-1)
|
||||
lastChat.chat_status = 2
|
||||
this.setData({
|
||||
aiStatus: 3,
|
||||
chatList: this.data.chatList
|
||||
})
|
||||
await request.post("/ai/history/upload", {
|
||||
chat_content: lastChat.chat_content || 'nocontent',
|
||||
chat_status: 2,
|
||||
chat_uuid: chat_uuid
|
||||
})
|
||||
},
|
||||
//复制文字
|
||||
copy(e) {
|
||||
let { content, chat_content } = e.currentTarget.dataset.text
|
||||
console.log(e);
|
||||
if (!content) {
|
||||
content = chat_content
|
||||
}
|
||||
copyText(content)
|
||||
},
|
||||
})
|
||||
6
pages/chat/index.json
Normal file
6
pages/chat/index.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"towxml": "/towxml/towxml"
|
||||
},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
179
pages/chat/index.scss
Normal file
179
pages/chat/index.scss
Normal file
@@ -0,0 +1,179 @@
|
||||
@import "./makedown.scss";
|
||||
|
||||
.c-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
padding-bottom: var(--area-bottom);
|
||||
|
||||
.t-navbar__content {
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
}
|
||||
|
||||
.silder {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 30rpx 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.chat-item {
|
||||
padding: 10rpx 30rpx;
|
||||
|
||||
&+.chat-item {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.lawyer-info {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 0.75rem;
|
||||
|
||||
image {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 10rpx;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-image {
|
||||
width: 250rpx;
|
||||
height: 250rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
max-width: calc(100% - 100rpx);
|
||||
min-height: 60rpx;
|
||||
width: fit-content;
|
||||
padding: 20rpx;
|
||||
border-radius: 10rpx;
|
||||
font-size: 0.9rem;
|
||||
overflow: hidden;
|
||||
|
||||
.loading {
|
||||
height: 40rpx;
|
||||
width: 40rpx;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 var(--td-brand-color);
|
||||
animation: load 2s linear infinite alternate;
|
||||
}
|
||||
|
||||
.stop-tip {
|
||||
margin-top: 20rpx;
|
||||
font-size: 0.64rem;
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
.m-footer {
|
||||
margin-top: 10rpx;
|
||||
|
||||
.tip {
|
||||
@extend .stop-tip;
|
||||
margin-top: 0;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.sound-icon {
|
||||
color: var(--text-2);
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-warpper {
|
||||
font-size: 0.75rem;
|
||||
gap: 30rpx;
|
||||
margin-top: 20rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.quick-item {
|
||||
background-color: white;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 50rpx;
|
||||
border: 1px solid #dfdddd;
|
||||
|
||||
&.color {
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai-msg {
|
||||
.chat-message {
|
||||
background-color: white;
|
||||
border-radius: 20rpx 30rpx 30rpx 10rpx;
|
||||
box-shadow: 0 0 15rpx #e2e2e2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.user-msg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
.chat-message {
|
||||
background-color: var(--td-brand-color);
|
||||
color: white;
|
||||
border-radius: 30rpx 30rpx 0 30rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-position {
|
||||
margin: 0 30rpx;
|
||||
position: relative;
|
||||
|
||||
//输入框
|
||||
.input-warpper {
|
||||
box-shadow: 0 0 15rpx #e2e2e2;
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: white;
|
||||
border-radius: 50rpx;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.push-btn {
|
||||
border-radius: 50%;
|
||||
width: 65rpx;
|
||||
height: 65rpx;
|
||||
color: var(--text-3);
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&.active-push {
|
||||
background-color: var(--td-brand-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.stop-btn {
|
||||
font-size: 65rpx;
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//占位
|
||||
.empty {
|
||||
transition: height 0.3s;
|
||||
}
|
||||
|
||||
//ai响应loading
|
||||
@keyframes load {
|
||||
0% {
|
||||
box-shadow: inset -20rpx 40rpx 0 var(--td-brand-color);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: inset 20rpx -40rpx 0 var(--td-brand-color);
|
||||
}
|
||||
}
|
||||
103
pages/chat/index.wxml
Normal file
103
pages/chat/index.wxml
Normal file
@@ -0,0 +1,103 @@
|
||||
<page-meta root-font-size="system"></page-meta>
|
||||
|
||||
<view class="bg-gradient c-container">
|
||||
<t-navbar class="custom-top-navbar"
|
||||
fixed="{{false}}"
|
||||
title="AI健康管家">
|
||||
</t-navbar>
|
||||
<scroll-view class="silder"
|
||||
scroll-top="{{scrollTop}}"
|
||||
scroll-y>
|
||||
<view class="chat-item {{item.chat_type == 1 ? 'user-msg' : 'ai-msg'}}"
|
||||
id="chat-${{index}}"
|
||||
wx:for="{{chatList}}"
|
||||
wx:key="index">
|
||||
<view class="lawyer-info flex-align"
|
||||
wx:if="{{item.chat_type !=1}}">
|
||||
<text>AI助手</text>
|
||||
</view>
|
||||
<!-- 显示图片格式 -->
|
||||
<view wx:if="{{item.con_type == 2}}">
|
||||
<image src="{{item.chat_content}}"
|
||||
class="chat-image"
|
||||
data-url="{{item.chat_content}}"
|
||||
bind:tap="viewImg"
|
||||
mode="aspectFill" />
|
||||
</view>
|
||||
<view class="chat-message "
|
||||
wx:else>
|
||||
<view class="loading"
|
||||
wx:if="{{item.loading}}"></view>
|
||||
<view wx:elif="{{item.tool}}">
|
||||
{{item.tool}}
|
||||
</view>
|
||||
<view wx:elif="{{item.chat_type == 1}}">
|
||||
{{item.chat_content}}
|
||||
</view>
|
||||
<towxml wx:elif="{{item.chat_type == 2}}"
|
||||
data-text="{{item}}"
|
||||
bind:longpress="copy"
|
||||
nodes="{{item.md_content}}" />
|
||||
<!-- 底部功能 -->
|
||||
<view wx:if="{{item.chat_status == 2}}"
|
||||
class="stop-tip">
|
||||
(已停止)
|
||||
</view>
|
||||
<view class="m-footer flex-between"
|
||||
wx:elif="{{item.end}}">
|
||||
<view class="tip">
|
||||
AI助手仅提供基于康复指南的一般性建议,不构成医疗诊断。如遇紧急情况请立即就医
|
||||
</view>
|
||||
<view class="flex-align">
|
||||
<t-icon name="file-copy"
|
||||
class="sound-icon"
|
||||
data-text="{{item}}"
|
||||
bind:tap="copy" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 快捷按钮 -->
|
||||
<view class="quick-warpper"
|
||||
wx:if="{{item.quick_btn.length > 0 && item.end}}">
|
||||
<view class="quick-item quick-{{quick.type}} {{quick.color ? 'color' : ''}}"
|
||||
wx:for="{{item.quick_btn}}"
|
||||
style="background-color: {{quick.color}};"
|
||||
wx:for-item="quick"
|
||||
wx:key="index"
|
||||
bind:tap="handQuick"
|
||||
data-parent="{{item}}"
|
||||
data-data="{{quick}}">
|
||||
{{quick.text}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-position">
|
||||
<!-- 输入框 -->
|
||||
<view class="input-warpper flex-align">
|
||||
<input placeholder="有什么问题尽管问我"
|
||||
bindblur="bindkeyboardBlur"
|
||||
bindinput="changeInput"
|
||||
bindconfirm="submitInput"
|
||||
value="{{inputText}}"
|
||||
hold-keyboard="{{false}}"
|
||||
adjust-position="{{false}}"
|
||||
bindkeyboardheightchange="bindkeyboardheightchange" />
|
||||
|
||||
<view class="push-btn flex-center {{inputText !='' ? 'active-push' : ''}} "
|
||||
wx:if="{{aiStatus==3}}"
|
||||
bind:tap="submitInput">
|
||||
<t-icon name="send-filled" />
|
||||
</view>
|
||||
<t-icon name="stop-circle-filled"
|
||||
class="stop-btn"
|
||||
bind:tap="stopMessage"
|
||||
wx:else />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 占位撑高 -->
|
||||
<view style="height: {{keyboardBottom}}px;"
|
||||
class="empty"></view>
|
||||
</view>
|
||||
17
pages/chat/makedown.scss
Normal file
17
pages/chat/makedown.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
.h2w__main,
|
||||
.h2w__p {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
font-size: 0.9rem;
|
||||
|
||||
}
|
||||
|
||||
.h2w__li {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
|
||||
.h2w__h3 {
|
||||
font-size: 32rpx !important;
|
||||
margin-top: 20rpx !important;
|
||||
}
|
||||
15
pages/chat/read.md
Normal file
15
pages/chat/read.md
Normal file
@@ -0,0 +1,15 @@
|
||||
聊天列表类型
|
||||
``` typescript
|
||||
interface ChatType{
|
||||
loading: boolean, // 是否开始响应
|
||||
end:boolean,//响应输出完成
|
||||
chat_type: 1 | 2, // 1用户,2ai普通响应
|
||||
chat_content: string, //文本内容
|
||||
md_content: string, //md格式
|
||||
con_type:1, //消息类型,1文本,2图片
|
||||
tool:string, //用工具时的默认内容,为空代表不是工具
|
||||
chat_status?:number, //2停止
|
||||
quick_btn:[], //快捷菜单按钮,为空不显示
|
||||
drugList:[],//药品信息,只用来设置提醒
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user