⭐前言
大家好,我是yma16,本文分享微信小程序——实现对话模式(调用大模型图片生成)。
aigc图片生成
AIGC (Artificial Intelligence Generated Content) 可以生成各种类型的图片,包括风景、动物、人物、抽象等等。生成图片的过程通常是使用预训练的神经网络模型,该模型可以根据输入的文本或图像生成新的图片。
⭐ 后端接口封装
💖 使用axios调用api
koa封装axios请求
const axios = require('axios') const axiosInstance = (baseURL, headers) => { const instance = axios.create({ baseURL: baseURL, timeout: 20000, headers: {...headers } }); return instance } const postAction = (baseURL, path, headers, data) => { const http = axiosInstance(baseURL, headers) return http.post(path, data) } module.exports = { postAction }
💖 暴露koa接口
这里我调用的时掘金的bot-api
const Router = require('koa-router'); const router = new Router(); const { postAction } = require('../../utils/request/index'); const API_KEY = '你的apikey' const bot_id = '你的bot_id' // 和bot聊天 router.post('/chat/bot', async(ctx) => { try { const bodyParams = ctx.request.body const { user, query } = bodyParams console.log('bodyParams', bodyParams) const headers = { "Authorization": `Bearer ${API_KEY}`, "Content-Type": "application/json", "Host": 'api.coze.cn', "Connection": "keep-alive" } const data = { "bot_id": bot_id, "user": user, "query": query, } const baseUrl = "https://api.coze.cn" const path = '/open_api/v2/chat' const res = await postAction(baseUrl, path, headers, data) ctx.body = { code: res.status, data: res.data, msg: res.statusText }; } catch (r) { ctx.body = { code: 0, msg: r } } }); module.exports = router;
⭐ 前端的交互设计
💖 布局设计
界面布局设计(一对一的对话模式)
<view class="container-box"> <view class="chat-container" id="chat-container-id" style="width: 100%;"> <scroll-view scroll-y="true" class="scroll-answer" scroll-with-animation bindscrolltoupper="upper" bindscrolltolower="lower" bindscroll="scroll" scroll-into-view="{{toView}}" scroll-top="{{scrollTop}}" wx:if="{{ chatObjConfig.option&&chatObjConfig.option.length>0 }}"> <view wx:for="{{ chatObjConfig.option }}" wx:for-index="index" wx:for-item="item" wx:key="index" id="chat-mode{{index}}"> <view class="create-time"> {{item.createTime}} </view> <view class="form-request"> <view wx:if="{{!item.isEdit}}" class='questioned'> <view style="display: flex;text-align: right;flex-direction:row-reverse;"> <view class="questioned-box-container"> <view class='questioned-box' style="text-align: left;"> {{item.question}} </view> <view class='questioned-box-poly'> </view> <view style="text-align: right;line-height: 50px;display: flex;max-height: 50px;"> <view class='form-request-user'> <!-- {{currentUserInfo.nickName}} --> <image class="user-image" src="{{currentUserInfo.avatarUrl}}"></image> </view> </view> </view> </view> </view> </view> <view class="form-response" wx:if="{{!item.isEdit}}"> <view style="display: flex;"> <view style="line-height: 50px;"> <view class='form-response-user'> <image class="ai-image" src="{{aiConfig.avatarUrl}}"></image> <!-- {{aiConfig.nickName}} --> </view> </view> <view class="form-response-box-poly"> </view> <view class='form-response-box' style="overflow: auto;"> <towxml wx:key="index" nodes="{{item.answerMarkdown}}" style="position: relative;background: transparent;user-select: text;" /> </view> </view> <view style="display: flex;width: 100%;box-sizing: border-box;" wx:if="{{layoutConfig.isShowCopyBtn}}"> <view style="width: 70%;"> </view> <view style="width: 30%;text-align: center;"> <button class="copy-btn" size="mini" bindtap="copyBtn" data-response=" {{item.answer}}">{{layoutConfig.copyText}}</button> </view> </view> </view> </view> <view class="form-submit" wx:if="{{mode==='openAiUse'}}" style="width: 100%;"> </view> </scroll-view> <view wx:else class="scroll-answer"> <view class="create-time"> {{currenTime}} </view> <view style="display: flex;"> <view style="line-height: 50px;"> <view class='form-response-user'> <image class="ai-image" src="{{aiConfig.avatarUrl}}"></image> <!-- {{aiConfig.nickName}} --> </view> </view> <view class="form-response-box-poly"> </view> <view class="form-response-box" style="padding: 0 10px;"> {{layoutConfig.emptyText}} </view> </view> </view> <view class="bottom-box"> <view class='submit-input'> <textarea class='send-input' bindinput="bindKeyInput" placeholder="{{layoutConfig.searchText}}" bindconfirm="search" value="{{searchOpenAiText}}" disabled="{{isLoading||isTruth}}" /> </view> <view class='send-btn' type="primary" bindtap="search" loading="{{isLoading}}" disabled="{{isLoading}}">{{layoutConfig.sendText}}</view> </view> </view> </view>
样式设置
/* pages/aiBot/aiBot.wxss */ .container-box{ position: relative; width: 100vw; height: 100vh; background: rgb(245, 245, 245); overflow: hidden; box-sizing: border-box; } .container-box-article { position: relative; padding-top:0px; width: 100%; height: calc(100vh - 88px); box-shadow: inset 5px 5px #262626; overflow: auto; user-select: text; } .scroll-answer { height: calc(100vh - 100px); } .chat-container { width: 100%; height: 100vh; overflow-y: auto; overflow-x: hidden; position: relative; } .paste-btn { background: rgba(16, 116, 187); color: #ffffff; /* transform: scale(.7); */ border-radius: 5px; } .clear-btn { background: rgba(16, 116, 187); color: #ffffff; /* transform: scale(.7); */ border-radius: 5px; } .paste-btn:hover { border: none; background: rgb(221, 0, 66); } .user-image-box { width: 300px; text-align: center; align-items: center; } .user-image { position: relative; width: 15px; height: 15px; border-radius: 50%; background-color: transparent; background: transparent; } .ai-image { position: relative; width: 20px; height: 20px; border-radius: 50%; } .questioned-box-container { display: flex; } .questioned-box { position: relative; max-width: calc(100vw - 90px); height: auto; overflow-x: auto; background-color: rgb(255, 255, 255); border-radius: 10px; right: -5px; padding: 0 10px; z-index: 999; color: #333; font-family: PingFang SC, Lantinghei SC, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans-serif; font-weight: 300; font-size: 32rpx; user-select: text; box-shadow: -5rpx 3rpx 1rpx -4rpx #c8c3c3; } .questioned-box-poly { position: relative; top: 15px; width: 0; height: 0; border-radius: 5px; border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-left: 12px solid rgb(255, 255, 255); box-shadow: 0rpx 0rpx 0rpx 0rpx #c8c3c3; } .clear-paste-btn{ width:70%; display: flex; } .submit-input { box-shadow: 0 2rpx 5rpx 5rpx #c8c3c3; width: 70%; } .send-input { height: 60px; background: rgba(255, 255, 255, .8); width: 100%; height: 100px; position: relative; text-indent: 8px; /* padding-left: 5px; */ color: rgb(0, 114, 221); } .send-btn::after{ position: absolute; left:0; top:0; width: 100px; height: 100%; background-color: rgba(255, 255, 255, .8); } .up-down-btn{ width:30%; } .send-btn { box-shadow: 0 2rpx 5rpx 5rpx #c8c3c3; width: 30%; background-color:rgba(16, 116, 187); color: #ffffff; height: 100px; line-height: 100px; /* border-radius: 10px 0 0 10px; */ text-align: center; } .empty-reponse-msg { position: relative; max-width: calc(100vw - 90px); height: auto; overflow-x: auto; background-color: rgb(255, 255, 255); border-radius: 10px; left: -5px; padding: 0 10px; z-index: 999; color: #333; font-family: PingFang SC, Lantinghei SC, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans-serif; font-weight: 300; font-size: 32rpx; user-select: text; } .create-time { width: 100%; text-align: center; color: rgb(255, 255, 255); background: rgb(218, 218, 218); margin: 5px auto; box-shadow: inset 0 1rpx 2rpx 1rpx rgba(0, 0, 0, 0.2); } .form-response-user { background-color: rgba(0, 72, 94, 0); color: #fff; } .form-response-box-poly { position: relative; top: 15px; width: 0; height: 0; border-radius: 5px; border-top: 10px solid transparent; border-bottom: 10px solid transparent; border-right: 12px solid rgb(255, 255, 255); } .form-response-box { position: relative; max-width: calc(100vw - 50px); /* word-break:keep-all; */ /* white-space: pre-wrap; */ white-space: pre-line; height: auto; overflow-x: auto; background-color: rgb(255, 255, 255); border-radius: 10px; color: #333; font-family: PingFang SC, Lantinghei SC, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans-serif; font-weight: 300; font-size: 32rpx; left: -5px; box-sizing: content-box; z-index: 999; user-select: text; box-shadow: 5rpx 3rpx 1rpx -4rpx #c8c3c3; } .form-request { display: block; width: 100%; color: #fff; background-color: rgba(37, 0, 97, 0); line-height: 50px; } .form-response { position: relative; width: 100%; margin-top: 10px; display: block; margin-bottom: 10px; color: #fff; background-color: rgba(0, 72, 94, 0); box-sizing: border-box; min-height: 60px; } .form-response-user { background-color: rgba(0, 72, 94, 0); color: #fff; } .form-request-user { background-color: rgba(37, 0, 97, 0); color: #fff; } .bottom-box { display: flex; width: 100%; display: absolute; bottom: 100px; }
引入markdown
{ "usingComponents": { "towxml":"/towxml/towxml" } }
💖 页面逻辑
page逻辑
// pages/aiBot/aiBot.js const app = getApp(); Page({ /** * 页面的初始数据 */ data: { currentUserInfo: { nickName: '', avatarUrl: 'https://profile-avatar.csdnimg.cn/8bea3d4b0c56486691de8f54fb649fa4_qq_38870145.jpg!1', }, saveKey: 'aiBot', baseCloudUrl: app.remoteConfig.baseCloudUrl, password: "***", username: "***", token: '', currenTime: '', isLoading: false, searchOpenAiText: '画一只猫', chatObjConfig: { option: [ // { // question: '', // answer: '', // isEdit: true, // createTime: '' // } ], currentIndex: 0, errorMsg: 'openai的服务器异常!' }, layoutConfig: { showPasteBtn: false, showTopBtn: false, introduceText: 'api介绍', useText: '使用', returnText: '返回介绍', sendText: '发送', searchText: '请输入关键词进行对话', reportText: '复制数据', copyText: '复制', pasteText: '粘贴', upText: "↑", downText: "↓", errorMsg: 'bot ai服务器异常!', emptyText: '欢迎使用aibot', storageKey: 'openAiOptionsConfig', permissionTitle: '很抱歉您没有权限!', permissionContent: '请联系微信号:cse-yma16\r\n 需要1元开通权限\r\n1元可支持100条消息!', wxInfoImg: 'https://yongma16.xyz/staticFile/common/img/userInfo.png', limitMsgCount: 10, confirmText: '添加微信', cancelText: '返回' }, aiConfig: { avatarUrl: 'https://yongma16.xyz/staticFile/common/img/aiTop.jpg', bgUrl: 'https://yongma16.xyz/staticFile/common/img/aiBg.jpg', nickName: 'openai', }, }, getUserToken() { const that = this wx.showLoading({ title: 'gen token loading', }); wx.request({ url: this.data.baseCloudUrl + 'token/gen', method: 'POST', data: { username: this.data.username, password: this.data.password }, success: (res => { that.setData({ token: res.data.token }) wx.hideLoading() }), fail: r => { console.log('cloud r', r) wx.hideLoading() } }) }, getCurrentTime() { const now = new Date() const year = now.getFullYear() const month = now.getMonth() const date = now.getDate() const hour = now.getHours() const minutes = now.getMinutes() const second = now.getSeconds() const formatNum = (n) => { return n > 9 ? n.toString() : '0' + n } return `${year}-${formatNum(month + 1)}-${formatNum(date)} ${formatNum(hour)}:${formatNum(minutes)}:${formatNum(second)}` }, bindKeyInput(e) { console.log('e.detail.value', e.detail.value) this.setData({ searchOpenAiText: e.detail.value }) }, scrollToBottom() { const index = this.data.chatObjConfig.option.length - 1 this.setData({ toView: `chat-mode${index}` }) }, saveStore() { }, search(e) { this.scrollToBottom() if (this.data.isLoading) { wx.showModal({ cancelColor: 'cancelColor', title: '正在响应中,请稍等...' }) return } if (!this.data.searchOpenAiText) { wx.showModal({ cancelColor: 'cancelColor', title: '请输入!' }) return } wx.showLoading({ title: '加载中', }) this.setData({ isLoading: true }) const that = this return new Promise((resolve, reject) => { wx.request({ url: that.data.baseCloudUrl + '/chat/bot', method: 'POST', header: { Authorization: `bearer ${that.data.token}` }, data: { user: 'qwerqwre', query: that.data.searchOpenAiText }, success: (res) => { console.log(res, 'res') const data = res.data.data const option = that.data.chatObjConfig.option console.log('data', data) const choices = data.messages?.[2] const answer = choices?.content || that.data.layoutConfig.errorMsg option.push({ question: that.data.searchOpenAiText, answer: answer, answerMarkdown: app.changeMrkdownText(answer), createTime: that.getCurrentTime(), isEdit: false, }) const chatObjConfig = { option: option } that.setData({ isLoading: false, searchOpenAiText: '', chatObjConfig: chatObjConfig }) wx.hideLoading() that.scrollToBottom() resolve(res) console.log('that.data.chatObjConfig.option', that.data.chatObjConfig.option) that.saveStore() }, fail: error => { that.setData({ isLoading: false }) wx.hideLoading() wx.showModal({ cancelColor: 'cancelColor', title: '网络波动失败...' }) } }); }) }, /** * 生命周期函数--监听页面加载 */ onLoad(options) { const aiBotConfig = app.wxProgramConfig.aiBotConfig console.log('aiBotConfig', aiBotConfig) this.setData({ saveKey: aiBotConfig.saveKey, searchOpenAiText: aiBotConfig.searchOpenAiText, password: aiBotConfig.cloudPwd || "U2FsdGVkX1+jfEkF2OXTQ5iIG4mrYc5/TLOiIntyENU=", username: aiBotConfig.cloudEmail || "1575057249@qq.com", }) this.getUserToken() this.setData({ currenTime: this.getCurrentTime() }) const currentUserInfo = wx.getStorageSync('currentUserInfo') if (currentUserInfo && currentUserInfo.nickName) { console.log('currentUserInfo', currentUserInfo) this.setData({ currentUserInfo: currentUserInfo }) } // 缓存 const chatObjConfig = wx.getStorageSync(this.data.saveKey) if (chatObjConfig) { this.setData({ chatObjConfig: chatObjConfig, }) } }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady() { }, /** * 生命周期函数--监听页面显示 */ onShow() { }, /** * 生命周期函数--监听页面隐藏 */ onHide() { }, /** * 生命周期函数--监听页面卸载 */ onUnload() { // 缓存 if (this.data.chatObjConfig) { wx.setStorageSync(app.wxProgramConfig.aiBotConfig.saveKey, this.data.chatObjConfig) } }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh() { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom() { }, /** * 用户点击右上角分享 */ onShareAppMessage() { } })
⭐ 效果
生成的图片
提示词:在摸鱼的猫
⭐结束
本文分享到这结束,如有错误或者不足之处欢迎指出!