鴻蒙開發(fā):實現(xiàn)AI打字機效果

前言

代碼案例基于Api13。

目前哪個行業(yè)最火,非AI莫屬,deepseek發(fā)布之后,可以說,又把AI推上了一個新高度,在和AI進行詢問會話的時候,我們可以發(fā)現(xiàn),AI的回答都是以流式的效果進行展示的,也就是類似于打字機的效果,那么針對這種效果在實際的開發(fā)中是如何實現(xiàn)的呢?

具體的效果,根據(jù)業(yè)務情況而定,有兩種模式,一種主動的流式輸出,也就是數(shù)據(jù)以流式的形式進行返回,前端直接用組件加載即可,第二種就是刻意的流式展示,也就是在拿到數(shù)據(jù)之后,前端實現(xiàn)流式輸出,進行打字機展示。

打字機的效果,一般都是在會話聊天之中,也就是列表之中,在實際的開發(fā)中,還要兼顧到,流式輸出的數(shù)據(jù)加載是否會影響性能,頁面閃爍,最新的聊天信息可展示等問題。

主動的流式輸出

在一般的AI會話中,實現(xiàn)一個流式輸出,一般會采用SSE或者WebSocket協(xié)議,像OpenAI官網(wǎng),DeepSeek官網(wǎng)是采用SSE協(xié)議,當然,在實際的開發(fā)中,大家可以選擇自己適用的技術即可??蛻舳税l(fā)送問題之后,服務端檢索到內容,就會時時的返回內容,具體是返回連接式的內容,還是逐字返回,需要和服務端進行定義。連接式返回,客戶端只管加載,逐字返回,需要客戶端拼接。

@Entry
@Component
struct Index {
  @State message?: string = ""
  intervalID?: number

  /**
   *AUTHOR:AbnerMing
   *INTRODUCE:模擬請求網(wǎng)絡接口
   */
  doHttp(success: (message: string) => void) {
    let data = "具體的實現(xiàn)效果,根據(jù)業(yè)務情況而定,有兩種模式,一種主動的流式輸出,也就是數(shù)據(jù)以流式的形式進行返回,前端直接用組件加載即可,第二種就是刻意的流式展示,也就是在拿到數(shù)據(jù)之后,前端實現(xiàn)流式輸出,進行打字機展示。"
    let position: number = 0
    //模擬請求流式輸出
    this.intervalID = setInterval(() => {
      position = position + 2
      let message = data.substring(0, position)
      if (success != undefined) {
        success(message)
      }
      if (message.length >= data.length) {
        clearInterval(this.intervalID)
      }
    }, 100)
  }

  build() {
    Column() {
      Button("加載")
        .margin({ top: 10 })
        .onClick(() => {
          this.doHttp((message: string) => {
            this.message = message
          })
        })
      Text(this.message)
        .margin({ top: 20 })
        .width("100%")
    }.width("100%")
    .height("100%")
    .padding(10)
  }
}

我們可以看下演示的效果,實際的開發(fā)中,前端無須關注每次的返回字的長度。

被動的流式展示

所謂的被動,就是在已有數(shù)據(jù)的情況下,如何實現(xiàn)打字機的效果,這個比較的簡單,無非是開啟一個定時,以每隔多少時間,輸出多少字為主,時間和輸出字的長度都可以自己調節(jié),簡單案例如下,當然了,這種方式一般很少應用于實際的開發(fā),不過在客戶端有類似打字機效果的情況下可以使用。

@Entry
@Component
struct Index {
  @State message?: string = ""
  intervalID?: number

  build() {
    Column() {
      Button("加載")
        .margin({ top: 10 })
        .onClick(() => {
          this.start()
        })
      Text(this.message)
        .margin({ top: 20 })
        .width("100%")
    }.width("100%")
    .height("100%")
    .padding(10)
  }

  private start() {
    let data = "具體的實現(xiàn)效果,根據(jù)業(yè)務情況而定,有兩種模式,一種主動的流式輸出,也就是數(shù)據(jù)以流式的形式進行返回,前端直接用組件加載即可,第二種就是刻意的流式展示,也就是在拿到數(shù)據(jù)之后,前端實現(xiàn)流式輸出,進行打字機展示。"
    let position: number = 0
    this.intervalID = setInterval(() => {
      position = position + 2
      this.message = data.substring(0, position)
      if (this.message.length >= data.length) {
        clearInterval(this.intervalID)
      }
      console.log("======定時")
    }, 100)
  }
}

我們看下輸出效果,是不是有那種打字機的效果了,需要注意的是,定時關閉。

列表打印機效果

以上的效果都是以一個Text組件展示的情況,在實際的開發(fā)中,更多的是以左右會話形式,這時需要考慮的就多一點,比如會話定位在底部,流式展示時,不讓列表閃爍等等問題,那么都是需要考慮的。

下面簡單的實現(xiàn)一下聊天會話模式,所有的數(shù)據(jù)都是模擬的,UI簡單的繪制了一下,在實際的開發(fā)中,肯定比較精細。

import { KeyboardAvoidMode } from '@kit.ArkUI'

@Observed
export class MessageBean {
  leftMessage?: string
  rightMessage?: string
}

@Component
struct ChatView {
  @ObjectLink messageBean: MessageBean;

  build() {
    Column() {
      if (this.messageBean.rightMessage != undefined) {
        Row() {
          Text(this.messageBean.rightMessage)
            .margin({ right: 10 })
          Image($r("app.media.startIcon"))
            .id("user_logo")
            .border({ color: Color.Red, width: 1, radius: 20 })
            .width(20)
            .height(20)
        }.width("100%")
        .justifyContent(FlexAlign.End)
        .padding({ right: 10 })
      } else {
        Row() {
          Image($r("app.media.startIcon"))
            .border({ color: Color.Red, width: 1, radius: 20 })
            .width(20)
            .height(20)
          Text(this.messageBean.leftMessage)
            .margin({ left: 10 })
        }.alignItems(VerticalAlign.Top)
        .padding({ right: 20 })
      }

    }
  }
}

@Entry
@Component
struct Index {
  private scroller: Scroller = new Scroller()
  @State sendMessage: string = ""
  @State messageList: MessageBean[] = []
  intervalID?: number
  private isEnd: boolean = true

  aboutToAppear(): void {
    this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE)
  }

  /**
   *AUTHOR:AbnerMing
   *INTRODUCE:模擬請求網(wǎng)絡接口
   */
  doHttp(success: (message: string) => void) {
    let data = "具體的實現(xiàn)效果,根據(jù)業(yè)務情況而定,有兩種模式,一種主動的流式輸出,也就是數(shù)據(jù)以流式的形式進行返回,前端直接用組件加載即可,第二種就是刻意的流式展示,也就是在拿到數(shù)據(jù)之后,前端實現(xiàn)流式輸出,進行打字機展示。"
    let position: number = 0
    //模擬請求流式輸出
    this.intervalID = setInterval(() => {
      position = position + 2
      let message = data.substring(0, position)
      if (success != undefined) {
        success(message)
      }
      if (message.length >= data.length) {
        this.isEnd = true //模擬結束
        clearInterval(this.intervalID)
      }
    }, 100)
  }

  build() {
    RelativeContainer() {
      List({ space: 10, scroller: this.scroller }) {
        ForEach(this.messageList, (item: MessageBean) => {
          ListItem() {
            ChatView({ messageBean: item })
          }
        })
      }.margin({ top: 10 })
      .alignRules({
        top: {
          anchor: "__container__",
          align: VerticalAlign.Top
        },
        bottom: {
          anchor: "layout_send",
          align: VerticalAlign.Top
        }
      })

      RelativeContainer() {
        TextInput({ placeholder: "請輸入問題" })
          .onChange((text) => {
            this.sendMessage = text
          })
          .alignRules({
            left: {
              anchor: "__container__",
              align: HorizontalAlign.Start
            },
            right: {
              anchor: "btn_send",
              align: HorizontalAlign.Start
            }
          })
        Button("發(fā)送")
          .id("btn_send")
          .margin({ left: 10 })
          .alignRules({
            right: {
              anchor: "__container__",
              align: HorizontalAlign.End
            }
          }).onClick(() => {
          //發(fā)送
          if (this.isEnd) {
            this.isEnd = false
            let bean = new MessageBean()
            bean.rightMessage = this.sendMessage
            this.messageList.push(bean)
            this.scroller.scrollEdge(Edge.Bottom)
            //模擬接口返回數(shù)據(jù)
            let leftBean = new MessageBean()
            this.messageList.push(leftBean)
            this.doHttp((content) => {
              leftBean.leftMessage = content
            })
          }

        })
      }
      .height(40)
      .id("layout_send")
      .padding({ left: 10, right: 10 })
      .backgroundColor(Color.White)
      .alignRules({
        bottom: {
          anchor: "__container__",
          align: VerticalAlign.Bottom
        }
      })
    }.width("100%")
    .height("100%")
  }
}

運行之后,我們簡單測試一下:

一般會話列表的形式,有一點需要注意,那就是歷史記錄。

相關總結

需要注意的是,內容一般都是以markdown的形式輸出,也就是真實的數(shù)據(jù)中,內容都是有樣式的,比如加粗,圖片,表格等等,所以,不能以單一的Text組件進行展示,需要針對markdown文本適配。

打字機的效果,更多的是在服務端的數(shù)據(jù)輸出,客戶端,最主要的是針對數(shù)據(jù)的渲染。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容