mm-saito-1204
@mm-saito-1204 (Naoki Saito)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

【flutter_chat_ui】chatGPT-APIからの戻り値を綺麗に表示できない

解決したいこと

現在、chatGPT-APIからの戻り値をflutter_chat_uiを利用して、SSEでリアルタイムに表示したいと考えています。(実際のchatGPTのように、1文字ずつ順番に表示されていくようにしたいということです。)
しかし、1文字ごとにチャットUIが再表示されてガチャガチャしてしまいます。

発生している問題・エラー

下記のように文字が追加されていくのではなく、1文字ごとに再表示しているようで、画面がガチャガチャしています。
Videotogif.gif

該当する箇所

          // 'content'を取得して文字列を追加していく
          List<dynamic> choices = data["choices"];
          Map<String, dynamic> choice = choices[0];
          Map<String, dynamic> delta = choice["delta"];
          String content = delta["content"];

          returnMessageText += content;

          final returnMessage = types.TextMessage(
            author: _ai,
            createdAt: DateTime.now().millisecondsSinceEpoch,
            id: randomString(),
            text: returnMessageText,
          );
          setState(() {
            _messages[0] = returnMessage;
          });

ソースコード全体

import "dart:convert";
import 'package:flutter/material.dart';
import 'dart:math';

import "package:http/http.dart" as http;
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

/* 
 * チャット画面
 */
String randomString() {
  final random = Random.secure();
  final values = List<int>.generate(16, (i) => random.nextInt(255));
  return base64UrlEncode(values);
}

/*
 * チャットルーム画面を生成するクラス
 */
class ExecuteAIChatPage extends StatefulWidget {
  final bool isQuestion;

  const ExecuteAIChatPage({Key? key, required this.isQuestion})
      : super(key: key);

  @override
  State<ExecuteAIChatPage> createState() => ExecuteAIChatState();
}

/*
 * チャットルーム画面ウィジェットのクラス
 */
class ExecuteAIChatState extends State<ExecuteAIChatPage> {
  final List<types.Message> _messages = [];
  final _user = const types.User(id: '82091008-a484-4a89-ae75-a22bf8d6f3ac');
  final _ai = const types.User(id: 'otheruser');
  String returnMessageText = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Chat(
        user: _user,
        messages: _messages,
        onSendPressed: _handleSendPressed,
        l10n: const ChatL10nEn(
            emptyChatPlaceholder: 'メッセージがありません。\nAIに質問してみましょう',
            inputPlaceholder: 'AIに質問する'),
      ),
    );
  }

  void _handleSendPressed(types.PartialText message) async {
    final textMessage = types.TextMessage(
      author: _user,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: message.text,
    );
    setState(() {
      _messages.insert(0, textMessage);
    });

    final returnMessage = types.TextMessage(
      author: _ai,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: "",
    );
    setState(() {
      _messages.insert(0, returnMessage);
    });
    sendChatCompletionRequest(message.text);
  }

  /*
   * chatgpt-API アクセス
   */
  void sendChatCompletionRequest(String text) {
    final client = http.Client();

    var request = http.Request(
      'POST',
      Uri.parse('https://api.openai.com/v1/chat/completions'),
    );

    Map<String, String> header = {
      'accept': 'text/event-stream',
      'Authorization': 'Bearer ${dotenv.env['OPENAI_APIKEY']!}',
      'Content-Type': 'application/json'
    };

    header.forEach((key, value) {
      request.headers[key] = value;
    });

    Map<String, dynamic> body = {
      "model": "gpt-3.5-turbo",
      "messages": [
        {"role": "user", "content": text},
      ],
      "stream": true
    };

    request.body = jsonEncode(body);

    Future<http.StreamedResponse> response = client.send(request);

    response.asStream().listen((data) {
      // ByteStreamをStringに変換し、改行で分割して1行ずつ処理する
      data.stream
          .transform(const Utf8Decoder())
          .transform(const LineSplitter())
          .listen(
        (dataLine) {
          // dataLineが空の場合や、[DONE]が返ってきた場合は早期リターン
          if (dataLine.isEmpty || dataLine == 'data: [DONE]') {
            return;
          }

          // dataLineの中身は'data: 'で始まっているので、それを削除してからMapに変換
          final map = dataLine.replaceAll('data: ', '');
          Map<String, dynamic> data = json.decode(map);
          // finish_reasonがstopの場合は早期リターン
          if (data['choices'][0]['finish_reason'] == 'stop') {
            return;
          }

          // 'content'を取得して文字列を追加していく
          List<dynamic> choices = data["choices"];
          Map<String, dynamic> choice = choices[0];
          Map<String, dynamic> delta = choice["delta"];
          String content = delta["content"];

          returnMessageText += content;

          final returnMessage = types.TextMessage(
            author: _ai,
            createdAt: DateTime.now().millisecondsSinceEpoch,
            id: randomString(),
            text: returnMessageText,
          );
          setState(() {
            _messages[0] = returnMessage;
          });
        },
      );
    });
  }
}

参考にしているサイト

確認の程、よろしくお願いいたします。

0

2Answer

When invoking

setState(() {
  _messages[0] = returnMessage;
});

to set the message along with appended content, the chat widget will initially remove the old message widget and subsequently add the new message widget. Additionally, a 300ms animation is applied during the process of adding the new message. It appears that there is no available method to circumvent or disable this animation.

You can find the relevant code in the insertItem method:

_listKey.currentState?.insertItem(pos);
1Like

Your answer might help someone💌