5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【チャットAI】ChatGPT(model: GPT-4)を利用してフロントエンド+バックエンドを一気に実装してみた【プログラミング】

Last updated at Posted at 2023-03-15

概要

ChatGPTの新しいモデル「GPT-4」がついに発表されましたね。
フリーハンドで書いたイラストからHTMLやCSSを生成できるなど、プログラミングの作業に史上最大の革命を起こしそうです。ただ、画像をインプットとしてChatGPTを使う方法はまだ一般には公開されないようで、とても待ち遠しい気持ちになっています。

今回、GPT-4となったことよってContext Window(=コンテキスト窓≒扱える文字数や単語数)が4k→8Kに増えたため、本記事では、過去の記事でAPIと画面を別々に作っていた「ピザ注文画面」を、手順を考えてもらいながら一気に実装する方法で試したいと思います。

(過去記事)
【チャットAI】ChatGPTを利用してピザ注文画面をReactで作ってみた【プログラミング】
【チャットAI】ChatGPTを利用してピザ注文REST APIをGo言語で作ってみた【プログラミング】

尚、GPT-4はChatGPT Plusの利用登録($20/month)が必要です。
この記事では、ChatGPT PlusでModel: GPT-4 を利用して会話しながら開発した内容を掲載しています。

この記事で説明すること

私がChatGPTに入力した内容と、その応答を(ほぼ)そのまま記載します。
尚、この記事内容の目的は、ChatGPTの利用で、どれくらいコーディングを効率化できるかを確認することです。このため、ChatGPTの出力内容に完璧さは求めません。
実装の正しさというよりも、「こちらの意図を認識した実装になっているか? 漏れがないか?」を確認します。

この記事で説明しないこと

実行環境の構築手順の詳細は書きません。

ポイント

検証を実施した中で、素晴らしいと思った点、もうちょっとだと思った点を説明します。

1.素晴らしいと思った点

  • 同じような実装はChatGPT Model:GPT-3.5でも試していたのですが、フロントエンドとバックエンドを一連の会話で作るのは流石に難しく、ChatGPTでそれぞれスレッドを分けて個別に作る必要がありました。
    (例)GPT-3.5の場合、途中から実装内容が破綻したり、仕様が抜けたり、前後でメソッド名やAPIが矛盾したり、など。

  • しかし、Model: GPT-4 では作業を適度にブレイクダウンしてくれて、かつ、前半の会話の内容を後半でもしっかり覚えており、それなりに安心して任せられると思いました。

2.もうちょっとだと思った点

  • 依頼した仕様が勝手に読み替えられる場合があります。
    (例)一覧に「名前、価格、カロリー、数量」を表示するよう依頼したのに、数量だけ一覧と別の場所に実装された。

  • 実際の運用ではちょっと使えない仕様になっている場合があります。
    (例)バックエンドのAPIをピザ1種類ずつ受け取るように先に作ってしまったためか、フロントエンド側で1回の注文ボタンのクリックで無理やりループしてAPIに投げている。1トランザクションにならないのでエラーが起きても途中までは注文してしまう。

  • 勝手に実装を省略する場合があります。このため、後から「ここで省略したところを実装してください」と追加で依頼する必要があります。

  • 何度か試したところ、実質的に同じ内容の依頼をしていているのに、ちょっと言い回しを変えるだけで関係ないところが全然違う実装になる場合があります。
    (例)注文登録のAPIで、「金額」を注文データとして登録したりしなかったりする。(どちらでも仕様としては大きく間違っていないが。尚、今回は「金額」は登録しないバージョン)

  • GPT-3.5 に比べて、やはり応答はまだ遅いです。コーディング量が少ないなら、GPT-3.5の方がまだまだ快適に使えます。体感4倍くらい遅い気がする。(※ただし、現在は新しいものが公開されたばかりで混雑している可能性もあり)

作ったアプリの仕様

  • ピザの名前と価格、カロリー、数量を一覧表示します。
  • 利用者がピザの数量を入力すると、合計の価格とカロリーを一覧の下に表示します。
  • 注文ボタンを押すと、注文を登録します。

尚、テーブル構造から作ってもらってもよかったのですが、敢えてこちらからテーブルの構造を提案してみました。
テーブルの関連を自動的に把握してくれるか確かめたかったためですが、ちゃんと意図を組んでJOINしてくれました。

流れ

1.優秀なプログラマーになることを依頼してから、機能の仕様を説明し、手順を教えてもらう。
2.手順の実装内容を順番に教えてもらう。

実際にやり取りした内容

ここからが実際のやり取りです。
尚、実際の回答はソースコードの後に説明を付け加えてくれますが、そこはだいたい省いています。
また、Context Windowが増えたとはいえ、まだまだ回答の途中で止まるため、適宜「continue」と入力して続きの回答を促していますが、そこも省いて連結した回答を掲載しています。

0.アプリの仕様を説明する。

(私)

あなたは優秀なプログラマーです。
私はピザの注文受付画面を開発しようとしています。

テーブル構造は以下とします。

  • テーブル名: PIZZA_INFOS
  • カラム名: PIZZA_ID, NAME, CALORIE, PRODUCT_DESCRIPTION
  • 説明: ピザの情報を保持するテーブル
  • テーブル名: PIZZA_PRICES
  • カラム名: PIZZA_ID, START_DATE, END_DATE, PRICE
  • 説明: ピザのID、期間の開始日、終了日、価格を保持するテーブル

画面の仕様は以下とします。

  • ピザの名前と価格、カロリー、数量を一覧表示します。
  • 利用者がピザの数量を入力すると、合計の価格とカロリーを一覧の下に表示します。
  • 注文ボタンを押すと、注文を登録します。

DBはSQLite3を利用します。
APIはGo言語で実装します。
画面はReact+TypeScriptで実装します。

これを実現する際の手順を順番に教えてください。

(ChatGPT Plusの回答)

このプロジェクトを実現するための手順を以下に示します。各手順には、データベースのセットアップ、バックエンドAPIの実装、フロントエンドの実装が含まれます。

1.データベースのセットアップ
1-1. SQLite3データベースファイルを作成します。
1-2. PIZZA_INFOSおよびPIZZA_PRICESテーブルを作成するSQLスクリプトを用意します。
1-3. SQLスクリプトを実行して、テーブルをデータベースに作成します。
1-4. テストデータを挿入します。

2.バックエンドAPIの実装 (Go言語)
2-1. Go言語で新しいプロジェクトを作成します。
2-2. 必要なライブラリをインポートします(例: "github.com/mattn/go-sqlite3", "github.com/gorilla/mux")。
2-3. SQLite3データベースへの接続を管理するパッケージを作成します。
2-4. ピザ情報を取得するAPIエンドポイント(GET /pizzas)を実装します。
2-5. 注文を登録するAPIエンドポイント(POST /orders)を実装します。
2-6. APIサーバーを起動して、エンドポイントが正常に動作することを確認します。

3.フロントエンドの実装 (React + TypeScript)
3-1. React + TypeScriptのプロジェクトを作成します(例: Create React Appを使用)。
3-2. 必要なライブラリをインポートします(例: "axios")。
3-3. ピザ情報を表示するコンポーネントを作成します。
3-4. 数量を入力するフォームと、合計の価格とカロリーを表示するコンポーネントを作成します。
3-5. 注文ボタンを押すと、注文を登録する処理を実装します。
3-6. ピザ情報を取得するAPIエンドポイント(GET /pizzas)を呼び出し、表示するコンポーネントにデータを渡します。
3-7. 注文を登録するAPIエンドポイント(POST /orders)を呼び出す処理を実装します。
3-8. フロントエンドアプリケーションをビルドし、動作を確認します。

以上の手順で、ピザの注文受付画面の開発が完了します。

頼もしいですね。かなり細かく手順を説明してくれています。
教えてもらった通りの手順で、より具体的な実装手順を教えてもらいながら進めましょう。

1.データベースのセットアップ

(私)

以下の具体的な実装内容を教えてください。
1-1. SQLite3データベースファイルを作成します。
1-2. PIZZA_INFOSおよびPIZZA_PRICESテーブルを作成するSQLスクリプトを用意します。
1-3. SQLスクリプトを実行して、テーブルをデータベースに作成します。

(ChatGPTの回答)

以下に、指定された手順の具体的な実装内容を示します。

1-1. SQLite3データベースファイルを作成します。

ターミナルで次のコマンドを実行し、SQLite3データベースファイルを作成します。

sqlite3 pizza_database.db

1-2. PIZZA_INFOSおよびPIZZA_PRICESテーブルを作成するSQLスクリプトを用意します。
新しいファイル(例:create_tables.sql)に、以下のSQLスクリプトを記述します。


-- PIZZA_INFOS テーブルを作成
CREATE TABLE IF NOT EXISTS PIZZA_INFOS (
  PIZZA_ID INTEGER PRIMARY KEY,
  NAME TEXT NOT NULL,
  CALORIE INTEGER NOT NULL,
  PRODUCT_DESCRIPTION TEXT
);

-- PIZZA_PRICES テーブルを作成
CREATE TABLE IF NOT EXISTS PIZZA_PRICES (
  PIZZA_ID INTEGER NOT NULL,
  START_DATE TEXT NOT NULL,
  END_DATE TEXT NOT NULL,
  PRICE REAL NOT NULL,
  FOREIGN KEY (PIZZA_ID) REFERENCES PIZZA_INFOS(PIZZA_ID)
);

1-3. SQLスクリプトを実行して、テーブルをデータベースに作成します。

ターミナルで次のコマンドを実行し、作成したSQLスクリプト(例:create_tables.sql)を実行してデータベースにテーブルを作成します。

sqlite3 pizza_database.db < create_tables.sql

内容的には過去記事とだいたい同じですね。
続けて、どんどん作ってもらいます。

2.バックエンドAPIの実装 (Go言語)

(私)

Go言語で新しいプロジェクトを作成は完了しました。
以下の具体的な実装内容を教えてください。

2-2. 必要なライブラリをインポートします(例: "github.com/mattn/go-sqlite3", "github.com/gorilla/mux")。
2-3. SQLite3データベースへの接続を管理するパッケージを作成します。
2-4. ピザ情報を取得するAPIエンドポイント(GET /pizzas)を実装します。
2-5. 注文を登録するAPIエンドポイント(POST /orders)を実装します。
2-6. APIサーバーを起動して、エンドポイントが正常に動作することを確認します。

(ChatGPTの回答)

以下に、指定された手順の具体的な実装内容を示します。

2-2. 必要なライブラリをインポートします。

ターミナルで次のコマンドを実行し、必要なライブラリをインストールします。

go get github.com/mattn/go-sqlite3
go get github.com/gorilla/mux

2-3. SQLite3データベースへの接続を管理するパッケージを作成します。

新しいファイル(例:db.go)に、以下のコードを記述します。

package main

import (
	"database/sql"
	_ "github.com/mattn/go-sqlite3"
)

func openDB() (*sql.DB, error) {
	db, err := sql.Open("sqlite3", "./pizza_database.db")
	return db, err
}

2-4. ピザ情報を取得するAPIエンドポイント(GET /pizzas)を実装します。

main.goファイルに、以下のコードを追加します。


package main

import (
	"encoding/json"
	"net/http"
	"github.com/gorilla/mux"
)

type PizzaInfo struct {
	PizzaID            int     `json:"pizza_id"`
	Name               string  `json:"name"`
	Calorie            int     `json:"calorie"`
	ProductDescription string  `json:"product_description"`
	Price              float64 `json:"price"`
}

func getPizzas(w http.ResponseWriter, r *http.Request) {
	db, err := openDB()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer db.Close()

	rows, err := db.Query(`
		SELECT
			i.PIZZA_ID,
			i.NAME,
			i.CALORIE,
			i.PRODUCT_DESCRIPTION,
			p.PRICE
		FROM
			PIZZA_INFOS i
			JOIN PIZZA_PRICES p ON i.PIZZA_ID = p.PIZZA_ID
		WHERE
			date('now') BETWEEN p.START_DATE AND p.END_DATE
	`)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	pizzas := make([]PizzaInfo, 0)

	for rows.Next() {
		var pizza PizzaInfo
		err := rows.Scan(&pizza.PizzaID, &pizza.Name, &pizza.Calorie, &pizza.ProductDescription, &pizza.Price)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		pizzas = append(pizzas, pizza)
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(pizzas)
}

2-5. 注文を登録するAPIエンドポイント(POST /orders)を実装します。

main.goファイルに、以下のコードを追加します。

type Order struct {
	PizzaID int `json:"pizza_id"`
	Amount  int `json:"amount"`
}

func addOrder(w http.ResponseWriter, r *http.Request) {
	var order Order
	err := json.NewDecoder(r.Body).Decode(&order)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// 注文情報をデータベースに保存する処理(省略)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(order)
}

2-6. APIサーバーを起動して、エンドポイントが正常に動作することを確認します。

main.goファイルに、以下のコードを追加してAPIサーバーを起動します。

func main() {
	router := mux.NewRouter()

	router.HandleFunc("/pizzas", getPizzas).Methods("GET")
	router.HandleFunc("/orders", addOrder).Methods("POST")

	http.ListenAndServe(":8080", router)
}

素晴らしいですね。
テーブルを意図的に「ピザ情報」と「ピザ価格」に分けて、使い方は教えなかったのですが、ちゃんとJOINしてくれています。(ここは過去記事と同じではありますが)
ちなみに、ここで「main.go」の全体のソースを教えてください、と質問すれば、ちゃんと繋げたものを教えてくれます。

尚、コメントで「// 注文情報をデータベースに保存する処理(省略)」とか手抜きされたので、実装を依頼します。

(私)

以下の処理を省略せずに実装してください。

「注文情報をデータベースに保存する処理」

(ChatGPTの回答)

バックエンド(Go言語)で注文情報をデータベースに保存する処理を実装します。まず、注文情報を格納するためのテーブルを作成し、次に注文を登録するAPIエンドポイントを実装します。

注文情報を格納するテーブルを作成します。
SQLite3データベースに注文情報を格納するためのテーブルを作成します。

テーブル構造は以下とします。

  • テーブル名: ORDERS
  • カラム名: ORDER_ID, PIZZA_ID, QUANTITY, ORDER_DATE
  • 説明: 注文情報を保持するテーブル
  • テーブル作成用のSQLスクリプトを用意し、実行してテーブルをデータベースに作成します。

注文を登録するAPIエンドポイント(POST /orders)を実装します。
Go言語で注文情報をデータベースに保存するAPIエンドポイントを実装します。以下のコードを main.go に追加してください。


import (
	// ...
	"time"
	// ...
)

type Order struct {
	PizzaID int `json:"pizza_id"`
	Quantity int `json:"quantity"`
}

func createOrder(w http.ResponseWriter, r *http.Request) {
	var order Order
	err := json.NewDecoder(r.Body).Decode(&order)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// 現在の日時を取得
	now := time.Now()

	// 注文情報をデータベースに保存
	stmt, err := db.Prepare("INSERT INTO orders (pizza_id, quantity, order_date) VALUES (?, ?, ?)")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer stmt.Close()

	_, err = stmt.Exec(order.PizzaID, order.Quantity, now)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// 登録成功
	w.WriteHeader(http.StatusCreated)
}

// main関数内に追加
func main() {
	// ...
	router.HandleFunc("/orders", createOrder).Methods("POST")
	// ...
}

この実装により、注文情報がデータベースに保存されるようになります。フロントエンド(React + TypeScript)の handleOrder 関数から POST /orders エンドポイントを呼び出すことで、注文情報をデータベースに登録できるようになります。

注文情報にtime.Nowを入れてくれています。
ピザの価格は日によって変わるので、後から請求内容を確認するには日付が必要になります。それを意図してくれたのかな? だったら非常に優秀ですね。

3.フロントエンドの実装 (React + TypeScript)

(私)

React + TypeScriptのプロジェクトを作成しました。
以下の具体的な実装内容を教えてください。

3-2. 必要なライブラリをインポートします(例: "axios")。
3-3. ピザ情報を表示するコンポーネントを作成します。
3-4. 数量を入力するフォームと、合計の価格とカロリーを表示するコンポーネントを作成します。
3-5. 注文ボタンを押すと、注文を登録する処理を実装します。
3-6. ピザ情報を取得するAPIエンドポイント(GET /pizzas)を呼び出し、表示するコンポーネントにデータを渡します。
3-7. 注文を登録するAPIエンドポイント(POST /orders)を呼び出す処理を実装します。
3-8. フロントエンドアプリケーションをビルドし、動作を確認します。

(ChatGPTの回答)

以下に、指定された手順の具体的な実装内容を示します。

3-2. 必要なライブラリをインポートします。

ターミナルで次のコマンドを実行し、必要なライブラリ(axios)をインストールします。

npm install axios

3-3. ピザ情報を表示するコンポーネントを作成します。
新しいファイル(例: PizzaList.tsx)に、以下のコードを記述します。


import React from 'react';

interface PizzaInfo {
  pizza_id: number;
  name: string;
  calorie: number;
  product_description: string;
  price: number;
}

interface PizzaListProps {
  pizzas: PizzaInfo[];
}

export const PizzaList: React.FC<PizzaListProps> = ({ pizzas }) => {
  return (
    <div>
      {pizzas.map((pizza) => (
        <div key={pizza.pizza_id}>
          <h3>{pizza.name}</h3>
          <p>カロリー: {pizza.calorie}</p>
          <p>価格: {pizza.price}</p>
        </div>
      ))}
    </div>
  );
};

3-4. 数量を入力するフォームと、合計の価格とカロリーを表示するコンポーネントを作成します。
新しいファイル(例: OrderForm.tsx)に、以下のコードを記述します。


import React, { useState } from 'react';

interface PizzaInfo {
  pizza_id: number;
  name: string;
  calorie: number;
  product_description: string;
  price: number;
}

interface OrderFormProps {
  pizzas: PizzaInfo[];
  onOrder: (order: { pizza_id: number; amount: number }) => void;
}

export const OrderForm: React.FC<OrderFormProps> = ({ pizzas, onOrder }) => {
  const [amounts, setAmounts] = useState<number[]>(new Array(pizzas.length).fill(0));

  const handleAmountChange = (index: number, newAmount: number) => {
    const newAmounts = [...amounts];
    newAmounts[index] = newAmount;
    setAmounts(newAmounts);
  };

  const handleOrder = () => {
    amounts.forEach((amount, index) => {
      if (amount > 0) {
        onOrder({ pizza_id: pizzas[index].pizza_id, amount });
      }
    });
  };

  const totalCalorie = pizzas.reduce((sum, pizza, index) => sum + pizza.calorie * amounts[index], 0);
  const totalPrice = pizzas.reduce((sum, pizza, index) => sum + pizza.price * amounts[index], 0);

  return (
    <div>
      <h2>注文フォーム</h2>
      {pizzas.map((pizza, index) => (
        <div key={pizza.pizza_id}>
          <h3>{pizza.name}</h3>
          <input
            type="number"
            min="0"
            value={amounts[index]}
            onChange={(e) => handleAmountChange(index, parseInt(e.target.value))}
          />
        </div>
      ))}
      <div>
        <p>合計カロリー: {totalCalorie}</p>
        <p>合計価格: {totalPrice}</p>
      </div>
      <button onClick={handleOrder}>注文</button>
    </div>
  );
};

3-5. 注文ボタンを押すと、注文を登録する処理を実装します。
この処理は、上記の OrderForm.tsx の handleOrder 関数内に実装済みです。

3-6. ピザ情報を取得するAPIエンドポイント(GET /pizzas)を呼び出し、表示するコンポーネントにデータを渡します。
新しいファイル(例: App.tsx)に、以下のコードを記述します。


import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { PizzaList } from './PizzaList';
import { OrderForm } from './OrderForm';

interface PizzaInfo {
  pizza_id: number;
  name: string;
  calorie: number;
  product_description: string;
  price: number;
}

const App: React.FC = () => {
  const [pizzas, setPizzas] = useState<PizzaInfo[]>([]);

  useEffect(() => {
    const fetchPizzas = async () => {
      const response = await axios.get<PizzaInfo[]>('http://localhost:8080/pizzas');
      setPizzas(response.data);
    };
    fetchPizzas();
  }, []);

  const handleOrder = (order: { pizza_id: number; amount: number }) => {
    console.log('注文:', order);
    // ここで、注文を登録するAPIエンドポイント(POST /orders)を呼び出す処理を実装する
  };

  return (
    <div>
      <PizzaList pizzas={pizzas} />
      <OrderForm pizzas={pizzas} onOrder={handleOrder} />
    </div>
  );
};

3-7. 注文を登録するAPIエンドポイント(POST /orders)を呼び出す処理を実装します。
App.tsx の handleOrder 関数内に、以下のコードを追加します。

const handleOrder = async (order: { pizza_id: number; amount: number }) => {
  console.log('注文:', order);
  try {
    await axios.post('http://localhost:8080/orders', order);
    alert('注文が完了しました。');
  } catch (error) {
    alert('注文に失敗しました。');
  }
};

3-8. フロントエンドアプリケーションをビルドし、動作を確認します。
ターミナルで次のコマンドを実行し、アプリケーションを起動します。

npm start

アプリケーションが起動したら、ブラウザで http://localhost:3000 にアクセスして、動作を確認します。ピザ情報が表示され、数量を入力して注文ボタンを押すと、注文が登録されることを確認してください。

画面側を実装してもらってから気が付きましたが、注文登録のAPI側がピザ1種類ずつ受け取る仕様のため、注文ボタンを1回押すと、選んだピザの種類数だけ、ループしてAPIを呼び出しています。
ここはあんまりよくないですね。
また、ピザの一覧と、ピザの注文フォームがそれぞれ別になっており、ピザ名が何度も表示されます。ここは依頼と異なってしまっている点です。

4.テストデータの作成

せっかくなので、テストデータの作成も依頼しました。

(私)

各テーブルのテストデータのINSERT文を教えてください。

(ChatGPTの回答)

以下に各テーブルに挿入するためのテストデータのINSERT文を示します。これらのSQL文を実行することで、各テーブルにテストデータが追加されます。

PIZZA_INFOS テーブルのテストデータ:

INSERT INTO PIZZA_INFOS (PIZZA_ID, NAME, CALORIE, PRODUCT_DESCRIPTION) VALUES
(1, 'マルゲリータ', 800, 'シンプルなトマトソースとモッツァレラチーズのピザ'),
(2, 'ペペロニ', 900, 'スパイシーなペペロニとモッツァレラチーズのピザ'),
(3, 'ハワイアン', 850, '甘くて美味しいパイナップルとハムのピザ');
INSERT INTO PIZZA_PRICES (PIZZA_ID, START_DATE, END_DATE, PRICE) VALUES
(1, '2023-01-01', '2023-12-31', 1000),
(2, '2023-01-01', '2023-12-31', 1200),
(3, '2023-01-01', '2023-12-31', 1100);

敢えて、最初に説明してもらったピザ情報のテーブルのテストデータを最後に聞いてみましたが、ちゃんと答えてくれました。
Context Windowの8Kに入った、ってことかな? 扱える文字や単語数が増えると、依頼したいコンテキストが増えても対応してもらえて助かりますね。

まとめ

今回は、速報ということで、3/15早朝(日本時間)に公開されたChatGPTの新しいモデル「GPT-4」の実装を試してみました。
まだまだ拙い実装ではあるものの、こちらの意図した仕様をかなり的確に汲み取って実装してくれます。
ノーコード的に使えるかというとちょっと微妙なところですが、上手く指示してあげたり、間違った部分を指摘して直してもらえば、実用に耐えるコーディングがこれまでの何倍ものスピードで作成できると思います。

今後も、どのような依頼をすれば実用的で、かつ、それなりの規模の実装ができるか、調査していきたいと思います。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?