Laravel × Vue.js × ChatGPT APIでコンテンツ制作補助ツールを作ってみた

Laravel

こんにちは!

「ChatGPT APIの具体的な活用方法がわからない・・・」

「ChatGPT APIの活用例を知りたい・・・」

「ChatGPT APIの組み込み方がわからない・・・」

などの悩みがありませんか?

今回は、LaravelとVue.js、ChatGPT APIを使って実際にコンテンツを作成する際の補助ツールを作成した話をご紹介します。

こちらのツールを作成した目的としては、筆者がブログコンテンツを作成する際の補助ツールとして活用するために作成しました。

補助ツールは筆者がブログコンテンツを作成する際に実際に使っています😊

ChatGPT APIの具体的な活用例を知りたい方や、ChatGPT APIの組み込み方を知りたい方は是非とも参考にしてください。

それでは早速みていきましょう!

目標成果物

以下のように記事タイトル提案をツールで行えるようにします。

内容としてはそれほど難しくありません。

サクッと一日程度あれば完了できる内容です😊

前提

今回はChatGPT APIの組み込み方や活用方法を中心に解説していくため、以下のような本質ではない事項については説明しません。

  • Laravel環境構築
  • HTMLやCSSなど
  • Vue3の使い方の詳細
  • OpenAIの登録方法

技術構成

技術構成としては、以下です。

  • バックエンド
    • PHP/Laravel9系
  • フロントエンド
    • Vue3
  • データベース
    • MySQL
  • 開発環境
    • Docker
  • 外部API
    • ChatGPT API/gpt-3.5-turbo

注意点として、Chat GPT APIには多少料金がかかります。

ただし、今回使用するのはGPT3.5のターボ版なので、ほとんどお金はかかりません。かなり使用して1$というところです。

システム設計

機能一覧

  • タイトル提案機能
  • 検索意図分析機能
  • アウトライン作成機能

提案や分析は全てChatGPTに行ってもらいます😊

また、工夫として、一度使ったキーワードを再度入力した場合は過去の結果を表示するようにしましょう。

ChatGPT APIも一応料金がかかりますからね。できる限り節約しておきましょう。

画面設計

設計というほどのものではないですが、以下3画面があります。

  • タイトル提案画面
  • 検索意図調査画面
  • アウトライン作成画面

サイドバーをつけて、全ての画面を自由に行き来できるようにしておきましょう。

DB設計

以下のテーブルを作成します。

  • keywordsテーブル・・・入力したキーワードを保存します。
  • titlesテーブル・・・提案されたタイトルを保存します。

DBを使う目的をはっきりさせておきましょう。ChatGPT APIから返ってきたレスポンスをそのまま表示するだけでもツールとしては使えます。ただし、先述したように、一度入力したキーワードは使い回したいのと、過去の結果を参照できるようにしたいので、今回はDBに結果を保存するようにします。

DB作成

最初に必要なテーブルを用意するところから始めていきます。

今回は論理削除を使っていきます。論理削除とは、簡単にいうと物理的にレコードを削除せずに、deleted_atカラムに削除日時を入れることで、論理的に削除したことにするという削除方法のことです。

物理的に削除するわけではないので、いざという時にはデータを復活させることができます。

titlesテーブル作成

それでは、titlesテーブルを作成していきます。

php artisan make:migration create_titles_table

マイグレーションファイルが作成されるので、編集します。

public function up()
{
    Schema::create('titles', function (Blueprint $table) {
        $table->id();
        $table->integer('keyword_id')->comment('キーワードID');
        $table->text('title')->comment('記事タイトル');
        $table->timestamps();
        $table->softDeletes();
    });
}

/**
 * Reverse the migrations.
 *
 * @return void
 */
public function down()
{
    Schema::dropIfExists('titles');
}

論理削除を設定する場合は、以下の一文を忘れずに追加しておきましょう。

$table->softDeletes();

これがないと論理削除が設定できません。

keywordsテーブル作成

同様に、keywordsテーブルも作成していきます。

public function up()
{
    Schema::create('keywords', function (Blueprint $table) {
        $table->id();
        $table->string('keyword')->comment('キーワード');
        $table->timestamps();
        $table->softDeletes();
    });
}

/**
 * Reverse the migrations.
 *
 * @return void
 */
public function down()
{
    Schema::dropIfExists('keywords');
}

Vue.jsのセットアップ

ここからはLaravelにVue.jsを導入していきます。

Laravelの環境構築はすでに終えているものとして進めます。

DockerによるLaravel環境構築の方法を知りたい方は以下のリンクから確認することができます。

手軽な環境構築!DockerでのLaravel環境構築スタートガイド

Vue.js導入

まずはVue.jsの導入から進めていきます。今回使用するVue.jsのバージョンは3系になります。

npm install --save-dev @vitejs/plugin-vue@4.0.0

バージョン4.0.0を指定していますが、これは筆者の環境だと最新バージョンだと動作しなかったため、バージョン指定をしています。通常はバージョン指定しなくても問題ないはずです。

次に、plugin-vueをvite.config.jsに追記しておきます。

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue'; // 追加

export default defineConfig({
    plugins: [
        vue(), // 追加
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
});

今回はVueRouterも使用することになるため、インストールしておきます。

npm install vue-router

最後に、resources/js/bootstrap.jsでアプリケーションビルド時にVueインスタンスを組み込むようにしておきます。

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

app.mount("#app");

クライアント側は常にVue3によって画面を書き換えるため、Laravel側はどんなURLに遷移してもresources/views/welcome.blade.phpのビューファイルを返すように設定しなければなりません。そこで、routes/web.phpでこの設定しておきます。

Route::get('/{any?}', fn () => view('welcome'))->where('any', '.*');

次に、welcome.blade.phpを編集していきます。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Blog支援ツール</title>
        @vite(['resources/css/app.css', 'resources/js/app.js'])
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>

viteは、@viteでCSSやJSファイルを読み込むことができます。また、<div id="app"></div>が非常に重要で、このappというIDにVueインスタンスをマウントすることで、JSによるDOM書き換えが可能になります。

これで、Vue3の基本的なセットアップは完了しました。

コンポーネントの作成

先に画面のコンポーネントを作成しておきましょう。

ルートコンポーネント

まずはルートコンポーネントであるApp.vueファイルをresources/jsディレクトリ配下に作成します。

cd resources/js
touch App.vue

App.vueを編集します。

<script setup>
</script>

<template>
    <div class="main">
        <router-view />
    </div>
</template>

<style scoped>
</style>

筆者はダークモードのUIが個人的に好きなので、ダークモードのデザインにしておきましょう。

resources/css/app.cssを編集します。

body {
    margin: 0;
    background-color: rgb(40, 40, 40);
    color: #fff;
}

タイトル提案画面作成

タイトル提案画面を初期画面として作成していきます。

resources/js/Title.vueを作成します。

cd resources/js
touch Title.vue

Title.vueを編集していきます。

<script setup>
</script>

<template>
    <div class="title">
        <h1>タイトル提案画面</h1>
    </div>
</template>

<style scoped>
.title {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding: 20px 0 100px;
}
</style>

VueRouterのセットアップ

今回VueRouterを組み込んでいるので、resources/js/router.jsを作成しルーティングの設定を行います。

cd resources/js
touch router.js

router.jsを編集します。

import { createRouter, createWebHistory } from 'vue-router';
import Title from "./Title.vue";

const routes = [
    {
        path: '/',
        component: Title,
        name: "title"
    },
]

const router = createRouter({
    history: createWebHistory(),
    routes,
});

export default router;

次に、bootstrap.jsにVueRouterを設定します。

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router"; // 追加

const app = createApp(App);

app.use(router).mount("#app"); // use(router)を追加

これで、/にアクセスした際には、タイトル提案画面が初期表示されるはずです。

ブラウザ確認

ここまでで、一度ブラウザから確認してみましょう。ビルドします。

npm run dev

ビルドされたら、ローカルホストからブラウザで確認してみましょう。

タイトル提案画面が表示されたら成功です。

タイトル提案機能作成

ここからはChatGPT APIを使って、記事タイトルの提案機能を作成していきます。

コントローラ作成

まずはLaravel側でコントローラを作成していきましょう。

php artisan make:controller ChatGPTController

コントローラが作成できたら、suggestTitleアクションを作成していきます。

public function suggestTitle(Request $request)
{
    $request_keyword = $request->input('keyword');
    $prompt = "あなたは素晴らしいSEOマーケターです。あなたは今から「" . $request_keyword . "」という検索キーワードで検索表示上位を狙った記事を書くことになりました。どのようなタイトルが相応しいか、タイトル案を10個提示してください。";
    $response = $this->execChatgpt($prompt);
    return $response;
}

本当はDBへの保存処理やすでにキーワード登録されていた場合の結果の使い回し処理などを作成する必要がありますが、今はわかりやすくChatGPT APIからのレスポンスをそのまま返すように実装しています。

また、今後ChatGPT APIへリクエストする処理はタイトル提案、検索意図分析、アウトライン作成処理で共通で使い回すことになるため、共通処理として分離しておきます。

それでは、ChatGPT APIへのリクエスト処理を追加していきます。

use Illuminate\\Support\\Facades\\Http;

private function execChatgpt($prompt)
{
    $url = "<https://api.openai.com/v1/chat/completions>";
    $headers = [
        "Content-Type" => "application/json",
        "Authorization" => "Bearer " . env('OPENAI_API_KEY')
    ];
    $data = [
        "model" => "gpt-3.5-turbo",
        "messages" => [
            [
                "role" => "system",
                "content" => "日本語で答えてください"
            ],
            [
                "role" => "user",
                "content" => $prompt
            ]
        ]
    ];
    $response = Http::withHeaders($headers)->post($url, $data);
    if ($response->json('error')) {
        return $response->json('error')['message'];
    }
    return $response->json('choices')[0]['message']['content'];
}

ChatGPT APIでは、ChatGPTモデルの指定やシステム設定、プロンプトなどをリクエストします。

今回はLaravelのHttpファサードを使ってAPIを実行していますが、Guzzleなどを使っても良いと思います。

ChatGPT APIのより詳しい解説は以下をご覧ください。

Laravelで学ぶChatGPTクローン作成とAPI活用

最後に、.envにAPIキーを設定しておきます。

OPENAI_API_KEY=OPEN_AIのAPIキー

モデル作成

次に、Titleモデルも作成しておきます。

php artisan make:model Title

今回は論理削除という削除方法を採用したいので、SoftDeletesトレイトを追加しておく必要があります。

// 論理削除を有効にする
use HasFactory, SoftDeletes;

タイトル提案画面作成

タイトル提案画面で、ChatGPTからの提案を表示できるようにしていきましょう。

最初に、ChatGPTからの提案は基本的にMarkdown形式になっているので、Markdownで表示できるようにしていきます。

JavaScriptでMarkdownを表示するには、marked.jsを使うのがおすすめです。インストールしていきましょう。

npm install marked

markdown形式を読み込む場合は、以下のように使います。

<script>
  document.getElementById('content').innerHTML =
    marked.parse('# Marked in the browser\\n\\nRendered by **marked**.');
</script>

Title.vueを編集します。

<script setup>
import { ref } from 'vue';
import axios from 'axios';
import { marked } from 'marked';
import { onMounted } from 'vue';

const keyword = ref('');
const title = ref('');
const loading = ref(false);

async function suggestTitle() {
    loading.value = true;
    const response = await axios.post('<http://localhost:8000/api/chatgpt/title>', {
        keyword: keyword.value
    });
    // 改行で分割する
    title.value = marked.parse(response.data);
    loading.value = false;
    getTitles();
}
</script>

<template>
    <div class="title">
        <h1>タイトル提案画面</h1>
        <form @submit.prevent="suggestTitle" class="form">
            <input type="text" v-model="keyword" class="input" placeholder="キーワード" />
            <button type="submit" class="button">提案開始</button>
        </form>
        <div class="card">
            <h2>タイトル提案</h2>
            <p>ここに最新のタイトル提案が表示されます</p>
            <p v-html="title"></p>
            <div v-if="loading" class="loading">ローディング...</div>
        </div>
    </div>
</template>

<style scoped>
.title {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding: 20px 0 100px;
}
.form {
    margin-bottom: 20px;
}
</style>

ブラウザで確認

ここまでできたらタイトル提案機能を使うことができるはずです。

ブラウザで確認しましょう。

タイトル提案画面を開いたら、適当なキーワードで提案開始ボタンを押し、しばらく待ってレスポンスが返ってきたら成功です。

タイトル提案機能強化

最後に、タイトル提案機能を強化していきます。

まず、リクエストされたキーワードがすでにDBに存在していた場合は、その時の結果を返すようにします。

ChatGPTコントローラを編集します。

$keyword = Keyword::where('keyword', $request->input('keyword'))->first();
if ($keyword) {
    $title = Title::where('keyword_id', $keyword->id)->first();
    if ($title) {
        return $title->title;
    }
} else {
    $request_keyword = $request->input('keyword');
    $prompt = "あなたは素晴らしいSEOマーケターです。あなたは今から「" . $request_keyword . "」という検索キーワードで検索表示上位を狙った記事を書くことになりました。どのようなタイトルが相応しいか、タイトル案を10個提示してください。";
    $response = $this->execChatgpt($prompt);
    return $response;
}

次に、リクエストされたキーワードと提案されたタイトルをDBへ保存しておきます。

$keyword = new Keyword;
$keyword->keyword = $request->input('keyword');
$keyword->save(); // キーワード保存

$request_keyword = $request->input('keyword');
$prompt = "あなたは素晴らしいSEOマーケターです。あなたは今から「" . $request_keyword . "」という検索キーワードで検索表示上位を狙った記事を書くことになりました。どのようなタイトルが相応しいか、タイトル案を10個提示してください。";
$response = $this->execChatgpt($prompt);

$title = new Title;
$title->keyword_id = $keyword->id;
$title->title = $response;
$title->save(); // タイトル保存
return $response;

はい。これで結果をDBへ保存し、過去に使用されたキーワードの場合はChatGPT APIを使わずに、結果を使い回すことができるようになりました。

それでは、画面からも過去のキーワードを確認できるように表示してみましょう。

タイトル一覧機能追加

Title.vueを開いて編集します。

<script setup>
import { onMounted } from 'vue';
const titles = ref([]);

// 画面が読み込まれた時に最新のタイトル提案一覧を取得する
onMounted(() => {
    getTitles();
});

...
async function getTitles() {
    loading.value = true;
    const response = await axios.get('http://localhost:8000/api/titles');
    titles.value = response.data;
    loading.value = false;
}
</script>

<template>
...
<div class="card">
  <h2>最近のタイトル提案一覧</h2>
    <div class="card-tight" v-for="title in titles" :key="title.id">
      <p class="keyword-history">「{{ title.keyword }}」</p>
    </div>
</div>
...
</temaplate>

次に、タイトル一覧取得APIを作成します。

routes/api.phpからタイトル一覧のエンドポイントを設定します。

Route::get('/titles', 'App\Http\Controllers\TitleController@fetchTitles')->name('titles.fetchTitles');

次に、Titleコントローラを作成します。

php artisan make:controller TitleController

次に、Titleコントローラにタイトル一覧取得のアクションを追加します。

public function fetchTitles()
{
    $titles = Title::leftJoin('keywords', 'titles.keyword_id', '=', 'keywords.id')
        ->select('titles.id', 'titles.title', 'keywords.keyword')
        ->orderBy('titles.created_at', 'desc')
        ->limit(10)
        ->get();
    return response()->json($titles);
}

タイトルは、新しい順に10個ほど取得するようにしておきます。

はい、こちらで完成です!

最後にもう一度ブラウザで確認しておくと良いでしょう。

それでは、今回はここまでとします。

最後までご清聴ありがとうございました!😊

コメント

タイトルとURLをコピーしました