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

文章目录

    • ⭐前言
    • [⭐ 后端接口封装](#⭐ 后端接口封装)
      • [💖 使用axios调用api](#💖 使用axios调用api)
      • [💖 暴露koa接口](#💖 暴露koa接口)
    • [⭐ 前端的交互设计](#⭐ 前端的交互设计)
      • [💖 布局设计](#💖 布局设计)
      • [💖 页面逻辑](#💖 页面逻辑)
    • [⭐ 效果](#⭐ 效果)
    • ⭐结束

⭐前言

大家好,我是yma16,本文分享微信小程序------实现对话模式(调用大模型图片生成)。

aigc图片生成

AIGC (Artificial Intelligence Generated Content) 可以生成各种类型的图片,包括风景、动物、人物、抽象等等。生成图片的过程通常是使用预训练的神经网络模型,该模型可以根据输入的文本或图像生成新的图片。

⭐ 后端接口封装

💖 使用axios调用api

koa封装axios请求

javascript 复制代码
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

javascript 复制代码
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;

⭐ 前端的交互设计

💖 布局设计

界面布局设计(一对一的对话模式)

html 复制代码
<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>

样式设置

css 复制代码
/* 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

json 复制代码
{
  "usingComponents": {
    "towxml":"/towxml/towxml"
  }
}

💖 页面逻辑

page逻辑

javascript 复制代码
// 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() {

  }
})

⭐ 效果

生成的图片


提示词:在摸鱼的猫

⭐结束

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

👍 点赞,是我创作的动力!

⭐️ 收藏,是我努力的方向!

✏️ 评论,是我进步的财富!

💖 最后,感谢你的阅读!

相关推荐
大厂码农老A1 小时前
3天实现"睡后收入"—— Cursor & Skills打造"全自动出海"Agent
人工智能·aigc·ai编程
树獭叔叔2 小时前
OpenClaw Agents 系统:多代理架构与智能编排的完整技术解析
后端·aigc·openai
树獭叔叔4 小时前
OpenClaw Workspace 文件完整指南:从文件到 AI 行为的完整链路
后端·aigc·openai
德育处主任5 小时前
『NAS』一句话生成网页,在NAS部署UPage
前端·javascript·aigc
刀法如飞6 小时前
AI时代,人人都是需求描述工程师
程序员·aigc·ai编程·需求文档
饼干哥哥17 小时前
这43个OpenClaw Skill,直接干翻跨境电商
aigc
饼干哥哥18 小时前
把n8n逼死后,Openclaw重构了跨境电商的内容创作流程
aigc
刀法如飞18 小时前
AI时代,程序员都应该是需求描述工程师
程序员·aigc·ai编程·需求文档
小兵张健18 小时前
白嫖党的至暗时期
人工智能·chatgpt·aigc
该用户已不存在1 天前
除了OpenClaw还有谁?五款安全且高效的开源AI智能体
人工智能·aigc·ai编程