微信小程序实现接入chatgpt 文心一言 moonshot 和豆包等大模型,然后实现流式返回数据渲染到页面上,实现打字机效果,支持重新生成回答和复制回答
使用了Vant组件 和 towxml来实现解析markdown 和实现代码高亮
效果图如下
js代码
const app = getApp();
Page({
data: {
inputText: '', // 用户输入的消息
chatHistory: [], // 存储聊天历史
currentChunk: '', // 存储当前正在接收的流式数据块
isLoading: false, // AI响应加载状态
},
onLoad: function() {
// 页面加载时初始化
},
// 处理输入框变化
onInput: function(e) {
this.setData({
inputText: e.detail.value,
});
},
// 发送用户消息并获取AI响应
sendMessage: function() {
const userMessage = this.data.inputText.trim();
if (!userMessage) return;
// 添加用户消息到聊天历史
this.addMessageToHistory(userMessage, 'user');
// 清空输入框
this.setData({
inputText: '',
isLoading: true,
currentChunk: '' // 清空当前流式数据的块
});
// 调用 AI 接口获取回复
this.requestAiResponse(userMessage);
},
// AI接口请求方法,复用的代码块
requestAiResponse: function(message) {
const self = this;
const requestTask = wx.request({
url: 'https://www.xxxx.com/chat.php', // 替换为你的AI API地址
method: 'POST',
data: {
message: message, // 发送提问到 AI
},
enableChunked: true,
success(res) {
// 处理成功响应
},
fail(err) {
console.error(err);
self.setData({ isLoading: false });
}
});
// 监听数据块接收事件
requestTask.onChunkReceived(function(res) {
try {
const chunkText = self.decode(res.data); // 处理ArrayBuffer
const dataLines = chunkText.trim().split('\n');
dataLines.forEach(line => {
if (line.startsWith('data: ')) {
const jsonString = line.slice(6); // 去掉 'data: ' 前缀
if (jsonString === '[DONE]') {
console.log('[DONE]');
return;
}
const chunkData = JSON.parse(jsonString);
if (chunkData.choices && chunkData.choices[0].delta && chunkData.choices[0].delta.content) {
const newContent = chunkData.choices[0].delta.content;
// 将新内容追加到当前正在处理的消息块中
self.data.currentChunk += newContent;
// 实时渲染新内容
self.renderChunk(self.data.currentChunk);
}
}
});
} catch (e) {
console.error('解析数据块失败:', res.data, e);
}
});
},
// 实时渲染当前内容
renderChunk: function(chunk) {
const self = this;
// 使用 Towxml 实时解析并渲染
const parsedMarkdown = app.towxml(chunk, 'markdown', {
theme: 'light',
events: {
tap: (e) => {
console.log('tap', e);
}
}
});
// 查找聊天记录中最新的 AI 消息并更新其内容
let lastIndex = self.data.chatHistory.length - 1;
let lastMessage = self.data.chatHistory[lastIndex];
if (lastMessage && lastMessage.sender === 'ai') {
// 更新最后一条 AI 消息的内容
lastMessage.markdownContent = parsedMarkdown;
lastMessage.text = chunk;
self.setData({
[`chatHistory[${lastIndex}]`]: lastMessage // 实时更新最后一条消息
});
} else {
// 如果是新的消息则添加到聊天历史中
self.addMessageToHistory(chunk, 'ai', parsedMarkdown);
}
},
// 将消息添加到聊天历史
addMessageToHistory: function(text, sender, markdownContent = '') {
const newMessage = {
text,
sender,
markdownContent
};
this.setData({
chatHistory: [...this.data.chatHistory, newMessage],
});
},
// 将 ArrayBuffer 转为字符串
decode: function(arrayBuffer) {
const uint8Array = new Uint8Array(arrayBuffer);
const encodedString = String.fromCharCode.apply(null, uint8Array);
const decodedString = decodeURIComponent(escape(encodedString)); // 解码为 UTF-8
return decodedString;
},
// 提取 markdownContent 中的纯文本
extractTextFromMarkdown: function(nodes) {
let textContent = '';
const traverse = (node) => {
if (typeof node === 'string') {
textContent += node;
} else if (Array.isArray(node)) {
node.forEach(traverse);
} else if (node.type === 'text') {
textContent += node.text;
} else if (node.children) {
node.children.forEach(traverse);
}
};
traverse(nodes);
return textContent.trim();
},
// 复制消息的函数
copyMessage: function(e) {
const index = e.currentTarget.dataset.index; // 获取消息的索引
const markdownNodes = this.data.chatHistory[index].markdownContent;
// 提取 markdown 内容中的纯文本
const textContent = this.extractTextFromMarkdown(markdownNodes);
wx.setClipboardData({
data: textContent,
success() {
wx.showToast({
title: '复制成功',
icon: 'none',
duration: 2000
});
},
fail() {
wx.showToast({
title: '复制失败',
icon: 'none',
duration: 2000
});
}
});
},
// 点击更新消息
updateMessage: function(e) {
const index = e.currentTarget.dataset.index; // 获取消息索引
const userMessage = this.data.chatHistory[index - 1]?.text; // 上一条是用户消息,获取用户消息的内容
if (userMessage) {
// 清空当前 AI 回答并显示加载状态
this.setData({
isLoading: true,
currentChunk: '', // 清空当前块数据
});
// 再次发送上次的问题
this.requestAiResponse(userMessage);
} else {
wx.showToast({
title: '没有找到用户问题',
icon: 'none',
duration: 2000
});
}
}
});
wxml代码
<view class="container">
<view class="chat-container">
<view class="chat-history">
<block wx:for="{{chatHistory}}" wx:key="index">
<view wx:if="{{item.sender === 'user'}}" class="chat-message user-message">
<view class="chat-message-container">
<view class="chat-message-item">
{{item.text}}
</view>
</view>
</view>
<view wx:if="{{item.sender === 'ai'}}" class="chat-message ai-message">
<view class="chat-message-container">
<view class="chat-message-item">
<towxml nodes="{{item.markdownContent}}" />
</view>
<view class="chat-input-actions">
<view class="chat-message-action copy" bindtap="copyMessage" data-index="{{index}}">
<van-icon class="copy" class-prefix="icon" name="copy" />
</view>
<view class="chat-message-action update" bindtap="updateMessage" data-index="{{index}}">
<van-icon class="update" class-prefix="icon" name="update" />
</view>
</view>
</view>
</view>
</block>
</view>
<!-- 输入区域 -->
<view class="chat-input-panel">
<view class="chat-input">
<input type="text" confirm-type="send" bindinput="onInput" placeholder="输入你的问题..." value="{{inputText}}" />
<button bindtap="sendMessage"><van-icon class="copy" class-prefix="icon" name="send" /></button>
</view>
</view>
</view>
</view>
wxss代码
page {
background-color: #fff;
height: 100%;
}
.h2w {
border-radius: 10rpx;
}
.h2w .h2w__main {
padding: 0rpx;
}
.h2w .h2w__p {
margin: 0;
}
.container {
height: 100%;
display: flex;
flex-direction: column;
}
.chat-container {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
}
.h2w.h2w-light {
background-color: #fff;
}
.chat-history {
background-color: #eff2f5;
flex: 1 1;
overflow: auto;
overflow-x: hidden;
padding: 30rpx;
position: relative;
overscroll-behavior: none;
}
.chat-message {
margin-bottom: 30rpx;
}
.user-message {
max-width: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.chat-message-item {
background-color: #465cff;
color: #fff;
padding: 20rpx;
max-width: 100%;
border-radius: 10rpx;
word-break: break-word;
}
.ai-message .chat-message-item {
background-color: #fff;
border: 4rpx solid #c6ccfc;
border-radius: 20rpx;
}
.ai-message {
display: flex;
flex-direction: row;
}
.chat-input-actions{
display: flex;
gap: 16rpx;
}
.chat-message-action {
margin-top: 20rpx;
background-color: #fff;
width: 50rpx;
height: 50rpx;
border-radius: 50%;
align-items: center;
justify-content: center;
display: flex;
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, .05);
}
.chat-input-panel {
position: relative;
width: 100%;
padding: 10px 20px 20px;
box-sizing: border-box;
padding: 10px;
border-top: 1px solid #dedede;
background-color: #fff;
}
.chat-input {
display: flex;
}
.chat-input input {
background-color: #fff;
flex-grow: 1;
padding: 20rpx;
border: 1px solid #dedede;
border-radius: 10rpx 0 0 10rpx;
margin-right: -1rpx;
}
.chat-input button {
padding: 0rpx 30rpx;
background-color: #465cff;
color: #fff;
border-radius: 0 10rpx 10rpx 0;
font-size: 22rpx;
}