Интеграция ChatGPT во Flutter

Мы кратко расскажем:

  1. Локальное создание сервера ChatGPT
  2. Создание пользовательского интерфейса ChatGPT
  3. Встроить пользовательский интерфейс ChatGPT во Flutter

Локальное создание сервера ChatGPT

Чтобы создать наш локальный хост-сервер, мы будем использовать NodeJS. Создаем папку под названием server и запускаем

npm init -y

Это создает файл package.json в папке server. Затем мы устанавливаем такие зависимости, как

npm install @openai/api express cors body-parser

После этого мы создаем файл с именем server.js и начинаем писать в нем наш серверный код. Но сначала, прежде чем мы начнем программировать

нам требуется ключ API OpenAI.

Создайте учетную запись на веб-сайте OpenAI, нажав здесь. Далее мы переходим к View API Keys

Здесь вы создаете свой секретный ключ, и все!!!!!

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

import express from 'express'
import * as dotenv from 'dotenv'
import cors from 'cors'
import { Configuration, OpenAIApi } from 'openai'

dotenv.config()

Затем мы создаем файл с именем .env и внутри него создаем переменную с именем OPENAI_API_KEY со значением secretKey, полученным из ключей OpenAI.

Теперь создадим объект конфигурации с помощью пакета OpenAI.

const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});

const openai = new OpenAIApi(configuration);

const app = express()
app.use(cors())
app.use(express.json())

app.post('/', async (req, res) => {
  try {
    const prompt = req.body.prompt;

    const response = await openai.createCompletion({
      model: "text-davinci-003",
      prompt: `${prompt}`,
      temperature: 0, 
      max_tokens: 3000, 
      top_p: 1, 
      frequency_penalty: 0.5,
      presence_penalty: 0, 
    });

    res.status(200).send({
      bot: response.data.choices[0].text
    });

  } catch (error) {
    console.error(error)
    res.status(500).send(error || 'Something went wrong');
  }
})

app.listen(5001, () => console.log('https://localhost:5001'))

Параметры конфигурации для отправки вызовов API в API OpenAI хранятся и управляются объектом Configuration. Для выполнения успешных запросов API он содержит такие параметры, как API key, endpoint URL и request timeouts. Указываем наш apiKey, который мы получили с сайта Open AI.

Объект OpenAIApi представляет собой класс в библиотеке OpenAI Node.js, предоставляющий методы для взаимодействия с API OpenAI. Он используется для аутентификации ключа API, настройки запроса API и выполнения вызовов API на серверы OpenAI.

Отдельное спасибо FeedSpot, за номинацию меня на их сайте! Проверьте FeedSpot здесь для захватывающего контента Flutter !!🙌 🎉🎉

Далее мы настраиваем экспресс-сервер и добавляем необходимое промежуточное ПО. В OpenAI доступны различные модели, но мы выбрали text-davinci-003 их для нашего случая.

Разновидностью языковой модели OpenAI GPT (Generative Pre-trained Transformer) является модель text-davinci-003. Он специально обучен для задач генерации естественного языка и обладает большими возможностями для понимания и создания текста, подобного человеческому. Модель была оптимизирована для различных задач, включая завершение текста, подведение итогов, ответы на вопросы и многое другое. Он был обучен на огромном количестве разнообразных текстовых данных.

Затем мы создаем POSTroute для ChatGPT с именем / и отправляем ответ обратно клиенту.

await openai.createCompletion({
  model: "text-davinci-003",
  prompt: `${prompt}`,
  temperature: 0, 
  max_tokens: 3000, 
  top_p: 1, 
  frequency_penalty: 0.5,
  presence_penalty: 0, 
}); 

Этот фрагмент использует API OpenAI для создания дополнений текста на основе заданной подсказки. Он создает завершение, отправляя запрос к API OpenAI со следующими параметрами:

  • model: Модель OpenAI для использования text-davinci-003 в нашем случае
  • prompt: текстовое приглашение для создания завершения.
  • temperature: от 0 до 1. Более высокие значения означают, что модель будет больше рисковать при создании текста.
  • max_tokens: максимальное количество токенов (слов или знаков препинания), которое может сгенерировать модель.
  • top_p: выбирает наиболее вероятные токены, пока не будет достигнута определенная пороговая вероятность.
  • frequency_penalty: значение штрафа от -2,0 до 2,0, которое наказывает модель за создание токенов, уже появившихся в сгенерированном тексте.
  • presence_penalty: значение штрафа от -2,0 до 2,0, которое наказывает модель за создание токенов, похожих на текст в подсказке.

Затем мы отправляем ответ JSON внутри свойства с именем «bot», значение которого извлекается из свойства «text» первого элемента в массиве «choices» объекта данных «response».

Мы слушаем входящие запросы на порту 5001 Наконец, мы запускаем сервер, выполнив следующую команду в терминале

# LISTEN TO THE PORT
app.listen(5001, () => console.log('https://localhost:5001'))

# TO RUN THE SERVER
npm run server

И если мы попытаемся свернуть приведенный выше URL-адрес с помощью примера запроса, мы получим следующее

# SEND REQUEST TO LOCALHOST
curl -X \
 POST "https://localhost:5001" -H \
 "accept: application/json" -H \
 "Content-Type: application/json" -d "{\"prompt\" : \"Hello\"}"

Получаем следующее:

Создание пользовательского интерфейса ChatGPT

Начнем с создания веб-проекта Flutter.

Необходимо

  • Мы должны использовать канал флаттера master
  • Версия Dart должна быть 3.0.0 или выше.

Давайте создадим проект, используя

flutter create chatgpt_embedding --platforms web

Примечание. Мы указываем platform как web, что означает, что проект поддерживает только web.

По умолчанию Flutter предоставляет нам приложение-счетчик.

Мы включаем пакет js в наш проект, чтобы обеспечить бесшовное взаимодействие кода JavaScript и Dart. Любая функция в вашем коде Dart может быть снабжена аннотацией @JSExportproperty с помощью js, что позволит вам вызывать ее из кода JavaScript.

flutter pub add js

Изменения в приложении счетчика

В нашем проекте всего 2 файла дротика, как показано ниже.

Мы реорганизуем main.dart и удалим всю логику счетчика. Затем мы создаем файл с именем gpt.dart. Он содержит MyHomePage, который является виджетом с отслеживанием состояния, и _MyHomePageState — это класс состояния для виджета MyHomePage.

class MyHomePage extends StatefulWidget {
  
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

@js.JSExport()
class _MyHomePageState extends State<MyHomePage> {}

Далее мы импортируем пакеты js и js_util как

import 'package:js/js.dart' as js;
import 'package:js/js_util.dart' as js_util;

_MyHomePageState аннотируется атрибутом @JSExport, так как нам нужно передать пользовательские запросы на сторону JavaScript. Это делает объект Dart _MyHomePageState экспортируемым.

Внутри нашего initState мы вызываем createDartExport для _MyHomePageState, который создает литерал объекта JS, который перенаправляется в наш экспортированный класс Dart (в нашем случае это _MyHomePageState)

Метод createDartExport() используется для создания объекта JavaScript, имеющего ссылку на объект Dart, к которому затем можно получить доступ из кода JavaScript. Методы setProperty() и callMethod() используются для настройки свойств JavaScript и вызова функций JavaScript соответственно.

void initState() {
  super.initState();
  final export = js_util.createDartExport(this);
  
  // These two are used inside the [js/js-interop.js]
  js_util.setProperty(js_util.globalThis, '_appState', export);
  js_util.callMethod<void>(js_util.globalThis, '_stateSet', []);
}

Экземпляр JsObject export создается путем вызова метода createDartExport(), предоставляемого библиотекой js_util. Это создает объект JavaScript, который имеет ссылку на экспортируемый объект Dart.

Два свойства JavaScript задаются с помощью метода setProperty(), предоставляемого js_util. Первое свойство _appState, для которого установлено значение export объекта, созданного на предыдущем шаге. Доступ к этому свойству можно получить из кода JavaScript для взаимодействия с объектом Dart.

Второе свойство_stateSet — это функция JavaScript, определенная в другом файле (js-interop.js). Эта функция вызывается с использованием метода callMethod(), предоставляемого js_util. Пустой массив [], переданный в качестве второго аргумента, указывает на то, что в функцию не передаются никакие аргументы.

Примечание. _appState и _stateSet присутствуют внутри файла js, о чем мы поговорим позже.

Мы создаем TextEditingController и FloatingActionButton внутри нашего gpt.dart. По сути, это будет принимать пользовательские запросы и передавать их в пользовательский интерфейс ChatGPT. Существует функция, которая вызывается, когда пользователь завершает запрос или когда пользователь нажимает кнопку FAB.

// This stores the user query
String _textQuery = '';

void textInputCallback(String value) {
   textFocusNode.requestFocus();
   setState(() {
     _textQuery = value;
     // This line makes sure the handler gets invoked
     _streamController.add(null);
   });
}
<script src="js/js-interop.js" defer></script>

Атрибут src указывает URL-адрес загружаемого файла JavaScript, в данном случае js-interop.js.

Все взаимодействие с Dart присутствует внутри этого файла. Мы используем IIFE для создания функции

(function () {
  window._stateSet = function () {
    console.log('HELLO From Flutter!!')
  };
}());

Этот код устанавливает функцию _stateSet как свойство глобального объекта window. Затем мы получаем доступ к _appState и сохраняем его внутри переменной внутри JS.

let appState = window._appState;

HTML и CSS для ChatGPT

Чтобы не удлинять статью, прикрепляю файлы css и html для ChatGPT UI.

Встроить пользовательский интерфейс ChatGPT во Flutter

Поскольку мы хотим показать приложение флаттера, мы создаем div с идентификатором flutter_target.

Мы настраиваем тег script внутри index.html, сначала добавляя прослушиватель событий к window. Получите идентификатор div, который представляет flutter app в нашем случае flutter_target, используя селектор запросов.

window.addEventListener("load", function (ev) {
  let target = document.querySelector("#flutter_target");
  
  _flutter.loader.loadEntrypoint({
    onEntrypointLoaded: async function (engineInitializer) {
      let appRunner = await engineInitializer.initializeEngine({
        hostElement: target,
      });
      await appRunner.runApp();
    },
  });
});
  • Используя _flutter.loader JavaScript API, предлагаемый flutter.js, мы изменяем способ запуска приложения Flutter в Интернете.

Следующие этапы составляют процесс инициализации:

  • Сценарий точки входа загружается, и сервисный работник инициализируется после получения сценария main.dart.js.
  • Инициализация механизма Flutter, который загружает необходимые файлы, включая CanvasKit, шрифты и ресурсы, для запуска веб-движка Flutter.
  • Запуск приложения, которое выполняет ваше приложение Flutter после подготовки для него DOM.

Взаимодействие между Javascript и Dart

Чтобы передать значение _textQuery из Flutter в JS, мы создаем функцию textQuery внутри файла gpt.dart и аннотируем ее свойством @JSExport (что делает ее доступной со стороны JS).

@js.JSExport()
String get textQuery => _textQuery;

Изменения JS

const form = document.querySelector('form')
const chatContainer = document.querySelector('#chat_container')
const formData = new FormData(form)

Выбираем первый элемент формы на странице методом document.querySelector(). Затем мы выбираем элемент HTML с идентификатором chat_container, используя метод document.querySelector().

Затем мы создаем новый экземпляр объекта FormData, используя переменную form. Этот FormData объект используется для извлечения данных из формы при ее отправке.

Мы создаем div с unique idкаждый раз, когда пользователь отправляет свои запросы из приложения Flutter.

Примечание. Идентификатор в основном представляет собой текущую отметку времени.

function chatStripe(isAi, value, uniqueId) {
    return (
        `
        <div class="wrapper ${isAi && 'ai'}">
            <div class="chat">
                <div class="profile">
                    <img 
                    src=${isAi ? './assets/bot.svg' : './assets/user.svg'} 
                    alt="${isAi ? 'bot' : 'user'}" 
                    />
                </div>
                <div class="message" id=${uniqueId}>${value}</div>
            </div>
        </div>
    `
    )
}

У нас также есть функция с именем chatStripe(), которая принимает три аргумента: isAi, value и uniqueId.

  • Аргумент isAi — это логическое значение, которое определяет, предназначена ли полоса чата для бота или пользователя. Если true, то для бота, а если false, то для пользователя.
  • Аргумент value — это текст сообщения, который будет отображаться в полосе чата.
  • Аргумент uniqueId — это уникальный идентификатор, который мы генерируем из приведенной выше функции.

Атрибут src элемента img устанавливается на путь к значку bot.svg или user.svg в зависимости от аргумента isAi.

Атрибут id элемента message div устанавливается равным аргументу uniqueId.

Наконец, у нас есть функция JavaScript, которая обрабатывает отправку форм. Функция запускается, когда пользователь отправляет сообщение боту, нажав кнопку отправки или нажав клавишу ввода в поле ввода текста.

  • Добавляем полосу чата пользователя в контейнер чата с помощью функции chatStripe()
  • Затем мы генерируем уникальный идентификатор с помощью функции generateUniqueId().

Добавить сервер

Мы создаем запрос POST на локальный сервер по адресу https://localhost:5001/ с сообщением пользователя в качестве полезной нагрузки JSON.

Примечание. Сервер был создан на первом шаге выше.

Если ответ сервера — OK, мы извлекаем ответ бота из данных JSON и обрезаем все конечные пробелы или символы новой строки с помощью метода trim().

И мы печатаем ответ бота в div сообщения, используя функцию typeText().

function typeText(element, text) {
    let index = 0

    let interval = setInterval(() => {
        if (index < text.length) {
            element.innerHTML += text.charAt(index)
            index++
        } else {
            clearInterval(interval)
        }
    }, 20)
}

Если ответ от сервера not OK, мы отображаем сообщение об ошибке в блоке сообщений и предупреждаем об ошибке.

Интеграция с флаттером

Мы определяем функцию с именем updateTextState, которая устанавливает для свойства prompt элемента формы значение appState.textQuery. Функция updateTextState не вызывается напрямую, а вместо этого регистрируется как обратный вызов с использованием метода appState.addHandler(). Это означает, что updateState функция будет вызываться каждый раз при изменении appState.

let updateTextState = function () {
   formData.set('prompt', appState.textQuery);
   handleSubmit.call(form)
};

// Register a callback to update the text field from Flutter.
appState.addHandler(updateTextState);

// CHAT GPT FUNCTIONS
form.addEventListener("submit", (e) => {
   handleSubmit(e)
});

// CHAT GPT FUNCTIONS
form.addEventListener("keyup", (e) => {
  if (e.keyCode === 13) {
    handleSubmit(e)
  }
});

Мы также подключаем прослушиватели событий для

  • submit событие элемента form. Он прослушивает отправку формы и вызывает функцию handleSubmit() с объектом event в качестве аргумента.
  • keyup событие элемента form. Он прослушивает нажатие пользователем клавиши Enter (keyCode 13) и вызывает функцию handleSubmit() с объектом event в качестве аргумента, который имеет тот же эффект, что и первый прослушиватель событий.

Функция handleSubmit() отвечает за обработку отправки формы, отображение сообщения пользователя, отправку его на сервер и отображение ответа сервера.

На стороне флаттера

Экземпляр StreamController _streamController определяется типом StreamController<void>.broadcast(). Метод broadcast() создает контроллер потока, который может обрабатывать несколько подписчиков.

final _streamController = StreamController<void>.broadcast();

@js.JSExport()
void addHandler(void Function() handler) {
  // This registers the handler we wrote in [js/js-interop.js]
  _streamController.stream.listen((event) {
    handler();
  });
}

Внутри метода создается StreamSubscription путем вызова метода listen() для _streamController.stream. Метод listen() принимает в качестве аргумента функцию обратного вызова, которая будет вызываться каждый раз при добавлении события в поток.

Затем мы модифицируем наши существующие функции и добавляем событие в streamController, чтобы убедиться, что handler, который мы написали в js-interop.js, вызывается.

Внутри метода setState() значение _textQuery устанавливается значением текстового контроллера, а затем экземпляр StreamController _streamController уведомляется об изменении, добавляя нулевое значение в поток с помощью _streamController.add(null).

@js.JSExport()
void textInputCallback(String value) {
  setState(() {
    _textQuery = value;
    // This line makes sure the handler gets invoked
    _streamController.add(null);
  });
}

Таким образом, пользовательский интерфейс обновляется всякий раз, когда передается textQuery со стороны Dart и отправляется на сторону JS.

"Исходный код"

Другие статьи:







Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу