(14-4-03)ChatGPT聊天系统(GPT-3.5/4.0+ Vue+pnpm+SQLite3):系统前端(3)

14.6.7  ChatGPT聊天主页面

在整个系统框架的右侧是ChatGPT聊天主页面,聊天主页面和ChatGPT官方的聊天页面高度相似,用户在文本框中发送聊天信息,ChatGPT获取聊天信息后返回给用户回答的信息。编写文件frontend/src/views/chat/index.vue实现本项目的ChatGPT聊天主页面,具体实现流程如下所示。

(1)编写方法handleSubmit()处理用户提交的聊天信息,具体实现代码如下所示。

function handleSubmit() {

  onConversation()

}

(2)编写异步方法onConversation()在聊天界面中应用户的输入,引用了NAutoComplete、NButton、NInput、NSwitch、useDialog和useMessage等Naive UI组件或钩子,用于在屏幕中央显示对话框、向用户显示提示信息以及监听用户输入事件等。同时,方法onConversation()还通过使用fetchChatAPIProcess()函数调用后端API来获取响应用户输入的聊天消息,并通过updateChat()、addChat()和updateChatSome()等函数修改聊天消息的状态,例如loading、error、conversationOptions和requestOptions等。最后,方法onConversation()还包括了一些自定义变量和函数,例如controller、ModelState、usingContext、scrollRef、prompt、dataSources、conversationList、promptStore、userInfo等,分别用于控制请求的取消、维护ChatGPT-4模型、切换上下文输入、滚动到底部、存储用户输入、获取数据源、过滤数据源、管理联想列表、获取用户信息等。方法onConversation()的具体实现代码如下所示、

async function onConversation() {
  let message = prompt.value

  if (loading.value)
    return

  if (!message || message.trim() === '')
    return

  controller = new AbortController()

  addChat(
    +uuid,
    {
      dateTime: new Date().toLocaleString(),
      text: message,
      inversion: true,
      error: false,
      conversationOptions: null,
      requestOptions: { prompt: message, options: null },
    },
  )
  scrollToBottom()

  loading.value = true
  prompt.value = ''

  let options: Chat.ConversationRequest = {}
  const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions

  if (lastContext && usingContext.value)
    options = { ...lastContext }

  addChat(
    +uuid,
    {
      dateTime: new Date().toLocaleString(),
      text: '思考中...',
      loading: true,
      inversion: false,
      error: false,
      conversationOptions: null,
      requestOptions: { prompt: message, options: { ...options } },
    },
  )
  scrollToBottom()

  try {
    let lastText = ''
    const fetchChatAPIOnce = async () => {
      await fetchChatAPIProcess<Chat.ConversationResponse>({
        prompt: message,
        options,
        baseURI: userInfo.value.baseURI,
        accessToken: userInfo.value.accessToken,
        isGPT4: ModelState.isGPT4,
        signal: controller.signal,
        onDownloadProgress: ({ event }) => {
          const xhr = event.target
          const { responseText } = xhr
          // Always process the final line
          const lastIndex = responseText.lastIndexOf('\n', responseText.length - 2)
          let chunk = responseText
          if (lastIndex !== -1)
            chunk = responseText.substring(lastIndex)
          try {
            const data = JSON.parse(chunk)
            updateChat(
              +uuid,
              dataSources.value.length - 1,
              {
                dateTime: new Date().toLocaleString(),
                text: lastText + (data.text ?? ''),
                inversion: false,
                error: false,
                loading: true,
                conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
                requestOptions: { prompt: message, options: { ...options } },
              },
            )

            if (openLongReply && data.detail.choices[0].finish_reason === 'length') {
              options.parentMessageId = data.id
              lastText = data.text
              message = ''
              return fetchChatAPIOnce()
            }

            scrollToBottomIfAtBottom()
          }
          catch (error) {
            //
          }
        },
      })
      updateChatSome(+uuid, dataSources.value.length - 1, { loading: false })
    }

    await fetchChatAPIOnce()
  }
  catch (error: any) {
    const errorMessage = error?.message ?? t('common.wrong')

    if (error.message === 'canceled') {
      updateChatSome(
        +uuid,
        dataSources.value.length - 1,
        {
          loading: false,
        },
      )
      scrollToBottomIfAtBottom()
      return
    }

    const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)

    if (currentChat?.text && currentChat.text !== '') {
      updateChatSome(
        +uuid,
        dataSources.value.length - 1,
        {
          text: `${currentChat.text}\n[${errorMessage}]`,
          error: false,
          loading: false,
        },
      )
      return
    }

    updateChat(
      +uuid,
      dataSources.value.length - 1,
      {
        dateTime: new Date().toLocaleString(),
        text: errorMessage,
        inversion: false,
        error: true,
        loading: false,
        conversationOptions: null,
        requestOptions: { prompt: message, options: { ...options } },
      },
    )
    scrollToBottomIfAtBottom()
  }
  finally {
    loading.value = false
  }
}

(3)编写异步方法onRegenerate(),用于重新生成聊天消息并响应用户输入。方法onRegenerate()参数index表示要重新生成的聊天消息在数据源中的索引位置。方法onRegenerate()使用了多个Vue.js实用工具来管理聊天消息和用户输入状态,例如computed、reactive和ref等。方法onRegenerate()还包括了一些自定义变量和函数,例如loading、userInfo、ModelState、openLongReply、controller等,分别用于保存加载状态、获取用户信息、维护GPT-4模型、开启长回答、控制请求的取消等。同时,方法onRegenerate()通过使用fetchChatAPIProcess()函数调用后端API来生成新的聊天消息,并通过updateChat()、updateChatSome()等函数修改聊天消息的状态,例如loading、error、conversationOptions和requestOptions等。最后,方法onRegenerate()使用try-catch-finally语句块捕获和处理异常情况,例如请求被取消、生成失败以及其他错误等情况。方法onRegenerate()的具体实现代码如下所示。

async function onRegenerate(index: number) {
  if (loading.value)
    return

  controller = new AbortController()

  const { requestOptions } = dataSources.value[index]

  let message = requestOptions?.prompt ?? ''

  let options: Chat.ConversationRequest = {}

  if (requestOptions.options)
    options = { ...requestOptions.options }

  loading.value = true

  updateChat(
    +uuid,
    index,
    {
      dateTime: new Date().toLocaleString(),
      text: '思考中...',
      inversion: false,
      error: false,
      loading: true,
      conversationOptions: null,
      requestOptions: { prompt: message, ...options },
    },
  )

  try {
    let lastText = ''
    const fetchChatAPIOnce = async () => {
      await fetchChatAPIProcess<Chat.ConversationResponse>({
        prompt: message,
        options,
        baseURI: userInfo.value.baseURI,
        accessToken: userInfo.value.accessToken,
        signal: controller.signal,
        isGPT4: ModelState.isGPT4,
        onDownloadProgress: ({ event }) => {
          const xhr = event.target
          const { responseText } = xhr
          // Always process the final line
          const lastIndex = responseText.lastIndexOf('\n', responseText.length - 2)
          let chunk = responseText
          if (lastIndex !== -1)
            chunk = responseText.substring(lastIndex)
          try {
            const data = JSON.parse(chunk)
            updateChat(
              +uuid,
              index,
              {
                dateTime: new Date().toLocaleString(),
                text: lastText + (data.text ?? ''),
                inversion: false,
                error: false,
                loading: true,
                conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
                requestOptions: { prompt: message, ...options },
              },
            )

            if (openLongReply && data.detail.choices[0].finish_reason === 'length') {
              options.parentMessageId = data.id
              lastText = data.text
              message = ''
              return fetchChatAPIOnce()
            }
          }
          catch (error) {
            //
          }
        },
      })
      updateChatSome(+uuid, index, { loading: false })
    }
    await fetchChatAPIOnce()
  }
  catch (error: any) {
    if (error.message === 'canceled') {
      updateChatSome(
        +uuid,
        index,
        {
          loading: false,
        },
      )
      return
    }

    const errorMessage = error?.message ?? t('common.wrong')

    updateChat(
      +uuid,
      index,
      {
        dateTime: new Date().toLocaleString(),
        text: errorMessage,
        inversion: false,
        error: true,
        loading: false,
        conversationOptions: null,
        requestOptions: { prompt: message, ...options },
      },
    )
  }
  finally {
    loading.value = false
  }
}

(4)编写方法handleExport(),功能是将聊天消息截图并导出为PNG格式的图片。该方法使用了多个Vue.js实用工具和Naive UI组件或钩子来显示对话框和提示信息,例如dialog、useMessage等。方法handleExport()的具体实现代码如下所示。

function handleExport() {
  if (loading.value)
    return

  const d = dialog.warning({
    title: t('chat.exportImage'),
    content: t('chat.exportImageConfirm'),
    positiveText: t('common.yes'),
    negativeText: t('common.no'),
    onPositiveClick: async () => {
      try {
        d.loading = true
        const ele = document.getElementById('image-wrapper')
        const canvas = await html2canvas(ele as HTMLDivElement, {
          useCORS: true,
        })
        const imgUrl = canvas.toDataURL('image/png')
        const tempLink = document.createElement('a')
        tempLink.style.display = 'none'
        tempLink.href = imgUrl
        tempLink.setAttribute('download', 'chat-shot.png')
        if (typeof tempLink.download === 'undefined')
          tempLink.setAttribute('target', '_blank')

        document.body.appendChild(tempLink)
        tempLink.click()
        document.body.removeChild(tempLink)
        window.URL.revokeObjectURL(imgUrl)
        d.loading = false
        ms.success(t('chat.exportSuccess'))
        Promise.resolve()
      }
      catch (error: any) {
        ms.error(t('chat.exportFailed'))
      }
      finally {
        d.loading = false
      }
    },
  })
}

对上述代码的具体说明如下:

  1. 方法handleExport()包括了一些自定义变量和函数,例如loading、html2canvas等,分别用于保存加载状态、生成截图等。
  2. 方法handleExport()使用HTMLCanvasElement API将指定区域的HTML元素转换为canvas,并通过toDataURL方法将canvas转换为Data URI,并使用createElement创建一个a标签,设置其href属性为Data URI,并设置download属性为指定的文件名。
  3. 最后,方法handleExport()通过将a标签添加到document.body中,并模拟用户点击a标签的click事件来触发下载操作,再移除a标签,并使用window.URL.revokeObjectURL方法释放资源。如果导出成功,则使用ms.success显示成功提示信息;否则使用ms.error显示失败提示信息。
  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值