Создание клона ChatGPT на Flutter с помощью OpenAI API

Создание клона ChatGPT на Flutter с помощью OpenAI API

21 февраля 2023 г.

ChatGPT (Generative Pre-trained Transformer) – это чат-бот, запущенный OpenAI в ноябре 2022 года. Он построен на основе семейства больших языковых моделей OpenAI GPT-3.5 и точно настроен как с помощью методов обучения с учителем, так и с помощью методов обучения с подкреплением.

ChatGPT был запущен в качестве прототипа 30 ноября 2022 года и быстро привлек внимание своими подробными и четкими ответами во многих областях знаний. Однако его неравномерная фактическая точность была определена как существенный недостаток.

В этой статье мы узнаем, как использовать API OpenAI для создания приложения ChatGPT на Flutter.

Для создания этого приложения нам понадобится следующее:

  1. Токен API: нам понадобится токен API от OpenAI. Вы можете получить токен API на панели управления учетной записи OpenAI. Если у вас нет учетной записи, вы можете создать ее.

2. http: пакет http flutter для обработки http-запросов.

3. provider: пакет Provider — это простой в использовании пакет, который по сути является оболочкой вокруг inheritedwidget, который упрощает использование и управление. Он предоставляет метод управления состоянием, который используется для управления частью данных в приложении.

4. анимированный текстовый набор: флаттер-пакет, содержащий коллекцию крутых текстовых анимаций.

5. flutter_svg: библиотека рендеринга SVG и виджетов для флаттера, которая позволяет размещать и отображать масштабируемую векторную графику. файлы.

Когда все готово, начинаем строить. 🍾 🍻

Откройте свой терминал и создайте свое приложение для флаттера, используя флаттер кли

flutter create openai-chat

Когда приложение будет создано, откройте папку в VSCode или любом другом текстовом редакторе, которым вы пользуетесь.

Откройте папку lib и откройте файл main, очистите первоначальный код, который был создан с приложением, потому что мы собираемся начать создавать наше приложение с нуля.

Ваш файл main.dart теперь будет выглядеть так, если создать виджет с отслеживанием состояния:

import 'package:flutter/material.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
     return MaterialApp(
        title: "Open AI Chat",
        home: SafeArea(
          bottom: true,
          top: false,
          child: Scaffold(
             backgroundColor: const Color(0xff343541),
             appBar: AppBar(
              backgroundColor: const Color(0xff343541),
              leading: IconButton(
              onPressed: () {},
              icon: const Icon(
                Icons.menu,
                color: Color(0xffd1d5db),
              ),
            ),
            elevation: 0,
            title: const Text("New Chat"),
            centerTitle: true,
            actions: [
              IconButton(
                onPressed: () {},
                icon: const Icon(
                  Icons.add,
                  color: Color(0xffd1d5db),
                ),
              ),
            ],
            ),
          body: Stack(
                    [],
            ),
          ),
        ), 
    );
  }
}

Теперь, когда мы настроили наше приложение, мы можем начать создавать все различные виджеты. Мы стремимся к четырем (4) различным виджетам:

  1. Виджет ввода данных пользователем
  2. Виджет сообщения пользователя
  3. Виджет сообщений AI
  4. Виджет загрузки

Создайте папку под названием widgets, в ней будут все четыре виджета, над которыми мы скоро будем работать.

  1. Виджет ввода данных пользователем

import 'package:flutter/material.dart';

class UserInput extends StatelessWidget {
  final TextEditingController chatcontroller;
  const UserInput({
    Key? key,
    required this.chatcontroller,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.bottomCenter,
      child: Container(
        padding: const EdgeInsets.only(
          top: 10,
          bottom: 10,
          left: 5,
          right: 5,
        ),
        decoration: const BoxDecoration(
          color: Color(0xff444654),
          border: Border(
            top: BorderSide(
              color: Color(0xffd1d5db),
              width: 0.5,
            ),
          ),
        ),
        child: Row(
          children: [
            Expanded(
              flex: 1,
              child: Image.asset(
                "images/avatar.png",
                height: 40,
              ),
            ),
            Expanded(
              flex: 5,
              child: TextFormField(
                onFieldSubmitted: (e) {

                },
                controller: chatcontroller,
                style: const TextStyle(
                  color: Colors.white,
                ),
                decoration: const InputDecoration(
                  focusColor: Colors.white,
                  filled: true,
                  fillColor: Color(0xff343541),
                  suffixIcon: Icon(
                    Icons.send,
                    color: Color(0xffacacbe),
                  ),
                  focusedBorder: OutlineInputBorder(
                    borderSide: BorderSide.none,
                    borderRadius: BorderRadius.all(
                      Radius.circular(5.0),
                    ),
                  ),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.all(
                      Radius.circular(5.0),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}    

UserInput принимает один параметр, chatcontroller. У нас также есть метод обратного вызова onFieldSubmitted, который поступает в проигрыватель, когда пользователь отправляет свое сообщение.

  1. Виджет сообщения пользователя

 class UserMessage extends StatelessWidget {
  final String text;
  const UserMessage({
    Key? key,
    required this.text,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(8),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            flex: 1,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Image.asset(
                "images/avatar.png",
                height: 40,
                width: 40,
                fit: BoxFit.contain,
              ),
            ),
          ),
          Expanded(
            flex: 5,
            child: Padding(
              padding: const EdgeInsets.only(
                left: 3,
                top: 8,
              ),
              child: Text(
                text,
                style: const TextStyle(
                  color: Color(0xffd1d5db),
                  fontSize: 16,
                  fontWeight: FontWeight.w700,
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Сообщение пользователя передает сообщение пользователя в качестве параметра классу Usermessage, который будет добавлен к ListView.

  1. Виджет сообщений AI

class AiMessage extends StatelessWidget {
  final String text;
  const AiMessage({
    Key? key,
    required this.text,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: const Color(0xff444654),
      padding: const EdgeInsets.all(8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            flex: 1,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Container(
                color: const Color(0xff0fa37f),
                padding: const EdgeInsets.all(3),
                child: SvgPicture.asset(
                  "images/ai-avatar.svg",
                  height: 30,
                  width: 30,
                  fit: BoxFit.contain,
                ),
              ),
            ),
          ),
          Expanded(
            flex: 5,
            child: AnimatedTextKit(
              animatedTexts: [
                TypewriterAnimatedText(
                  text,
                  textStyle: const TextStyle(
                    color: Color(0xffd1d5db),
                    fontSize: 16,
                    fontWeight: FontWeight.w700,
                  ),
                ),
              ],
              totalRepeatCount: 1,
            ),
          ),
        ],
      ),
    );
  }
}

Сообщение AI передает пользовательское сообщение в качестве параметра классу AiMessage, который будет добавлен к ListView.

С помощью пакета AnimatedTextKit мы можем анимировать наш текст, используя анимацию пишущей машинки.

  1. Виджет загрузки

 class Loading extends StatelessWidget {
  final String text;
  const Loading({
    Key? key,
    required this.text,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: const Color(0xff444654),
      padding: const EdgeInsets.all(8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            flex: 1,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Container(
                color: const Color(0xff0fa37f),
                padding: const EdgeInsets.all(3),
                child: SvgPicture.asset(
                  "images/ai-avatar.svg",
                  height: 30,
                  width: 30,
                  fit: BoxFit.contain,
                ),
              ),
            ),
          ),
          Expanded(
            flex: 5,
            child: Text(
              text,
              style: const TextStyle(
                color: Color(0xffd1d5db),
                fontSize: 16,
                fontWeight: FontWeight.w700,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Виджет загрузчика используется для ожидания ответа от вызова API, после завершения ответа мы удаляем загрузчик из списка.

Константа приложения

const endpoint = "https://api.openai.com/v1/";
const aiToken = "sk-------------------------------------";

Создайте файл с именем api_constants.dart, он будет содержать нашу конечную точку и токен API. Вы можете получить свой токен API на панели инструментов токенов API OpenAI.

Репозиторий OpenAI

class OpenAiRepository {
  static var client = http.Client();

  static Future<Map<String, dynamic>> sendMessage({required prompt}) async {
    try {
      var headers = {
        'Authorization': 'Bearer $aiToken',
        'Content-Type': 'application/json'
      };
      var request = http.Request('POST', Uri.parse('${endpoint}completions'));
      request.body = json.encode({
        "model": "text-davinci-003",
        "prompt": prompt,
        "temperature": 0,
        "max_tokens": 2000
      });
      request.headers.addAll(headers);

      http.StreamedResponse response = await request.send();
      if (response.statusCode == 200) {
        final data = await response.stream.bytesToString();

        return json.decode(data);
      } else {
        return {
          "status": false,
          "message": "Oops, there was an error",
        };
      }
    } catch (_) {
      return {
        "status": false,
        "message": "Oops, there was an error",
      };
    }
  }
}

Теперь давайте пообщаемся с API OpenAI. Мы должны создать файл с именем openai_repository.dart в папке репозитория. В файле у нас есть класс OpenAIRepository со статическим методом sendMessage, который принимает только один параметр prompt

.

Аутентификация

API OpenAI использует ключи API для аутентификации. Получите ключ API, который вы будете использовать в своих запросах.

Все запросы API должны включать ваш ключ API в заголовок HTTP Authorization следующим образом:

Authorization: Bearer YOUR_API_KEY

n Отправка запроса

{
  "model": "text-davinci-003",
  "prompt": prompt,
  "temperature": 0,
  "max_tokens": 2000
}

Этот запрос запрашивает модель Davinci для завершения текста, начиная с подсказки, которую вы отправили из пользовательского ввода. Параметр max_tokens устанавливает верхнюю границу того, сколько токенов будет возвращать API. температура означает, что модель будет подвергаться большему риску. Попробуйте 0,9 для более творческих приложений и 0 для тех, у кого есть четко определенный ответ.

Это вернет ответ Map, который выглядит следующим образом.

 {
    "id": "cmpl-GERzeJQ4lvqPk8SkZu4XMIuR",
    "object": "text_completion",
    "created": 1586839808,
    "model": "text-davinci:003",
    "choices": [
        {
            "text": "nnThis is indeed a test",
            "index": 0,
            "logprobs": null,
            "finish_reason": "length"
        }
    ],
    "usage": {
        "prompt_tokens": 5,
        "completion_tokens": 7,
        "total_tokens": 12
    }
}

Модель чата

class ChatModel extends ChangeNotifier {
  List<Widget> messages = [];

  List<Widget> get getMessages => messages;

  Future<void> sendChat(String txt) async {
    addUserMessage(txt);

    Map<String, dynamic> response =
        await OpenAiRepository.sendMessage(prompt: txt);
    String text = response['choices'][0]['text'];
    //remove the last item
    messages.removeLast();
    messages.add(AiMessage(text: text));

    notifyListeners();
  }

  void addUserMessage(txt) {
    messages.add(UserMessage(text: txt));
    messages.add(const Loading(text: "..."));
    notifyListeners();
  }
}

Поскольку мы используем провайдера в качестве нашего управления состоянием, мы создаем класс с именем ChatModel, который расширяет ChangeNotifier. Мы создаем пустой List, который мы будем использовать для отправки новые сообщения (виджет). геттер getMessages для получения сообщений

Мы создаем метод с именем sendChat, который принимает пользовательский ввод, а затем вызывает addUserMessage, который помещает новый виджет, содержащий сообщение пользователя, а также виджет-загрузчик в список сообщений.

Затем мы отправляем запрос в репозиторий OpenAI, который затем отправляет ответ. Затем мы сохраняем текст в переменную String с именем text

.

Затем мы удаляем виджет загрузчика из списка и добавляем виджет AIMessage

Почти готово… 🤞🏽

Мы должны вернуться к виджету userInput и вызвать sendChat, когда пользователь попытается отправить свое сообщение. Теперь ваш код будет выглядеть примерно так.

TextFormField(
  onFieldSubmitted: (e) {
    context.read<ChatModel>().sendChat(e);
    chatcontroller.clear();
  },

Нажми🚀

Все, что нужно сделать сейчас, это отредактировать наш файл main.dart. Оберните тело в MultiProvider, и ваш код будет выглядеть примерно так.

body: MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (_) => ChatModel()),
  ],
  child: Consumer<ChatModel>(builder: (context, model, child) {
    List<Widget> messages = model.getMessages;
    return Stack(
      children: [
        //chat

        Container(
          margin: const EdgeInsets.only(bottom: 80),
          child: ListView(
            children: [
              const Divider(
                color: Color(0xffd1d5db),
              ),
              for (int i = 0; i < messages.length; i++) messages[i]
            ],
          ),
        ),
        //input
        UserInput(
          chatcontroller: chatcontroller,
        )
      ],
    );
  }),
),

Приложение запущено 🛸🚁

Все готово, вы можете начать использовать ChatGPT в своем приложении Flutter. Вы также можете клонировать репозиторий прямо здесь.


Также опубликовано здесь.

Если у вас есть вопросы, оставьте свой комментарий здесь, и я отвечу на них как можно скорее.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE