微信小程序——实现对话模式(调用大模型图片生成)

简介: 微信小程序——实现对话模式(调用大模型图片生成)

⭐前言

大家好,我是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() {
  }
})

⭐ 效果

生成的图片

提示词:在摸鱼的猫

⭐结束

本文分享到这结束,如有错误或者不足之处欢迎指出!


目录
相关文章
|
1月前
|
小程序 JavaScript API
微信小程序开发之:保存图片到手机,使用uni-app 开发小程序;还有微信原生保存图片到手机
这篇文章介绍了如何在uni-app和微信小程序中实现将图片保存到用户手机相册的功能。
552 0
微信小程序开发之:保存图片到手机,使用uni-app 开发小程序;还有微信原生保存图片到手机
|
2月前
|
小程序
小程序消除图片下边距的三个方法
小程序消除图片下边距的三个方法
49 11
|
1月前
|
算法 小程序 Java
java制作海报三:获取微信二维码详情,并改变大小,合成到海报(另一张图片)上
这篇文章介绍了如何使用Java获取微信小程序的二维码,并将其调整大小后合成到海报(另一张图片)上。
41 0
|
3月前
|
小程序 前端开发
|
3月前
|
运维 小程序 前端开发
小程序开发问题之在小程序中调用my.chooseImage接口让用户选择图片如何解决
小程序开发问题之在小程序中调用my.chooseImage接口让用户选择图片如何解决
|
4月前
|
前端开发 小程序
【微信小程序-原生开发】实用教程20 - 生成海报(实战范例为生成活动海报,内含生成指定页面的小程序二维码,保存图片到手机,canvas 系列教程)
【微信小程序-原生开发】实用教程20 - 生成海报(实战范例为生成活动海报,内含生成指定页面的小程序二维码,保存图片到手机,canvas 系列教程)
409 0
|
4月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的图片推荐系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的图片推荐系统附带文章源码部署视频讲解等
46 0
|
4月前
|
小程序 JavaScript 安全
【微信小程序-原生开发】转发给好友/群,分享到朋友圈(含单页模式访问云开发数据库的方法)
【微信小程序-原生开发】转发给好友/群,分享到朋友圈(含单页模式访问云开发数据库的方法)
176 0
|
1月前
|
移动开发 小程序 数据可视化
基于npm CLI脚手架的uniapp项目创建、运行与打包全攻略(微信小程序、H5、APP全覆盖)
基于npm CLI脚手架的uniapp项目创建、运行与打包全攻略(微信小程序、H5、APP全覆盖)
217 3
|
1月前
|
小程序 API
微信小程序更新提醒uniapp
在小程序开发中,版本更新至关重要。本方案利用 `uni-app` 的 `uni.getUpdateManager()` API 在启动时检测版本更新,提示用户并提供立即更新选项,自动下载更新内容,并在更新完成后重启小程序以应用新版本。适用于微信小程序,确保用户始终使用最新版本。以下是实现步骤: ### 实现步骤 1. **创建更新方法**:在 `App.vue` 中创建 `updateApp` 方法用于检查小程序是否有新版本。 2. **测试**:添加编译模式并选择成功状态进行模拟测试。
48 0
微信小程序更新提醒uniapp