📑

ロジックツリー作成アプリ(判定付き)

2024/10/09に公開

はじめに

皆さん、大学や会社で「論理的に考えろ」と言われたことはありませんか?
私は論理的に考えることがかなり苦手な部類の人間です。今のところ様々なフレームワークを試すことで、改善に努めています。
その一つが「ロジックツリー」。抽象的概念から枝葉を広げていくことで、その事柄における要素の深さ・広さを視覚的に表現することが可能になります。
ですが、私はこの手法を用いていると疑問が浮かびます。

  • ノードごとのつながりは、地に足ついてるの?
  • 論理の飛躍はないの?
  • 他にもっと深掘りできないの?

そのためロジックツリーを視覚化しつつ、論理の整合性を判定できる状態を簡易アプリで実現したいと思います。

目指す要件定義

まずは、今回作成するアプリの要件を明確にしましょう。

  • ロジックツリーの作成: ユーザーが入力したテキストをノードとして、ツリー構造で表示します。
  • 論理性のチェック: ノードを追加する際、前のノードとの論理的なつながりをOpenAI APIを使ってチェックします。
  • ノードの追加方法の選択: ユーザーはノードを「子として追加」または「兄弟として追加」できます。
  • 任意のノードへの追加: ツリー内の任意のノードを選択し、そのノードに対して子や兄弟を追加できます。
  • エラーメッセージと候補の提示: 論理の飛躍がある場合、エラーメッセージと修正のための候補を表示します。

実装環境

  • フロントエンドフレームワーク: Next.js 14
  • スタイリング: Tailwind CSS
  • プログラミング言語: TypeScript
  • API: OpenAI API(GPT-4モデルを使用)

環境準備

1. Next.jsプロジェクトの作成

まずは、Next.jsのプロジェクトを作成します。

terminal
npx create-next-app@latest logic-tree-app --typescript --eslint
terminal
Need to install the following packages:
  create-next-app@14.2.14
Ok to proceed? (y) y
✔ Would you like to use ESLint?No / YesWould you like to use Tailwind CSS?No / YesWould you like to use `src/` directory?No / YesWould you like to use App Router? (recommended)No / YesWould you like to customize the default import alias (@/*)? … No / Yes
Creating a new Next.js app in /Users/username/my-logic-tree-app.

2. ディレクトリに移動

terminal
cd logic-tree-app

3. Tailwind CSSのセットアップ

tailwind.config.js を以下のように設定します。

tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}', // Next.js 13のappディレクトリを使用している場合
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

globals.css を全て上書きします。

app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

4. OpenAI APIのインストール

OpenAI APIを使用するため、ライブラリをインストールします。

terminal
npm install openai axios

5. 環境変数の設定

プロジェクトのルートに .env.local ファイルを作成し、OpenAIのAPIキーを設定します。

.env.local
OPENAI_API_KEY=your-openai-api-key

your-openai-api-key を実際のAPIキーに置き換えてください。


ディレクトリ構成

logic-tree-app/
├── .env.local
├── tailwind.config.js
├── postcss.config.js
├── package.json
├── tsconfig.json
├── src/
│   └── app/
│       ├── globals.css
│       ├── page.tsx
│       └── api/
│           └── checkLogic/
│               └── route.ts
├── node_modules/
└── ...

コード実装

page.tsx全体
src/app/page.tsx
'use client';

import { useState } from 'react';

interface Node {
  id: number;
  content: string;
  children: Node[];
}

export default function Home() {
  const [inputValue, setInputValue] = useState('');
  const [nodes, setNodes] = useState<Node[]>([]);
  const [errorMessage, setErrorMessage] = useState('');
  const [addAsChild, setAddAsChild] = useState(true);
  const [selectedNodeId, setSelectedNodeId] = useState<number | null>(null);

  const handleNodeClick = (nodeId: number) => {
    setSelectedNodeId(nodeId);
  };

  const handleSubmit = () => {
    if (!inputValue) return;

    const newNode: Node = {
      id: Date.now(), // 一意のIDを生成
      content: inputValue,
      children: [],
    };

    if (selectedNodeId === null) {
      // ノードが選択されていない場合、ルートに追加
      setNodes([...nodes, newNode]);
      setInputValue('');
      setErrorMessage('');
      return;
    }

    // 選択されたノードを検索
    const selectedNode = findNodeById(nodes, selectedNodeId);
    if (!selectedNode) {
      setErrorMessage('選択されたノードが見つかりません。');
      return;
    }

    if (addAsChild) {
      // 子として追加
      selectedNode.children.push(newNode);
    } else {
      // 兄弟として追加
      const parentNode = findParentNode(nodes, selectedNodeId);
      if (parentNode) {
        parentNode.children.push(newNode);
      } else {
        // ルートレベルに追加
        setNodes([...nodes, newNode]);
      }
    }
    setNodes([...nodes]);
    setInputValue('');
    setErrorMessage('');
  };

  const findNodeById = (nodes: Node[], id: number): Node | null => {
    for (const node of nodes) {
      if (node.id === id) return node;
      const childNode = findNodeById(node.children, id);
      if (childNode) return childNode;
    }
    return null;
  };

  const findParentNode = (
    nodes: Node[],
    id: number,
    parent: Node | null = null
  ): Node | null => {
    for (const node of nodes) {
      if (node.id === id) return parent;
      const parentNode = findParentNode(node.children, id, node);
      if (parentNode) return parentNode;
    }
    return null;
  };

  const findParentContent = (nodes: Node[], id: number): string | null => {
    const parentNode = findParentNode(nodes, id);
    return parentNode ? parentNode.content : null;
  };

  const renderTree = (nodes: Node[], level: number = 0) => {
    return (
      <div>
        {nodes.map((node) => (
          <div key={node.id}>
            <div
              onClick={() => handleNodeClick(node.id)}
              style={{
                marginLeft: level * 20,
                backgroundColor:
                  node.id === selectedNodeId ? '#e0e0e0' : 'transparent',
                cursor: 'pointer',
              }}
              className="border p-2 m-1"
            >
              {node.content}
            </div>
            {node.children.length > 0 && renderTree(node.children, level + 1)}
          </div>
        ))}
      </div>
    );
  };

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">ロジックツリー作成アプリ</h1>

      {selectedNodeId === null && (
        <p className="text-blue-500">ノードを選択してください。</p>
      )}

      {/* ノードの追加方法を選択 */}
      <div className="mb-4">
        <label className="mr-4">
          <input
            type="radio"
            name="addMethod"
            value="child"
            checked={addAsChild}
            onChange={() => setAddAsChild(true)}
            className="mr-1"
          />
          選択したノードの子として追加
        </label>
        <label>
          <input
            type="radio"
            name="addMethod"
            value="sibling"
            checked={!addAsChild}
            onChange={() => setAddAsChild(false)}
            className="mr-1"
          />
          選択したノードの兄弟として追加
        </label>
      </div>

      {/* 入力フィールドと送信ボタン */}
      <div className="mb-4">
        <input
          type="text"
          className="border p-2 w-full"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="テキストを入力してください"
        />
        <button
          onClick={handleSubmit}
          className="bg-blue-500 text-white px-4 py-2 mt-2"
        >
          送信
        </button>
      </div>

      {errorMessage && <p className="text-red-500">{errorMessage}</p>}

      {/* ロジックツリーの表示 */}
      <div className="mt-8">
        <h2 className="font-bold mb-2">ロジックツリー:</h2>
        {renderTree(nodes)}
      </div>
    </div>
  );
}

1. ベースとなるページの作成

app/page.tsx を作成し、基本的なページレイアウトを設定します。

src/app/page.tsx
'use client';

import { useState } from 'react';

export default function Home() {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">ロジックツリー作成アプリ</h1>
      {/* ここにコンテンツを追加していきます */}
    </div>
  );
}
  • 'use client'; は、Next.js 13でクライアントサイドレンダリングを明示するために使用します。
  • Home コンポーネント内に基本的なレイアウトを設定し、コンテンツを追加していく土台を作ります。

2. ノードのデータ構造を定義

src/app/page.tsx
interface Node {
  id: number;
  content: string;
  children: Node[];
}
  • ロジックツリーの各ノードを表す Node インターフェースを定義します。
    • id: ノードの一意な識別子。
    • content: ノードに表示されるテキスト。
    • children: 子ノードの配列。

3. 状態管理の追加

src/app/page.tsx
const [inputValue, setInputValue] = useState('');
const [nodes, setNodes] = useState<Node[]>([]);
const [suggestions, setSuggestions] = useState<string[]>([]);
const [errorMessage, setErrorMessage] = useState('');
const [addAsChild, setAddAsChild] = useState(true);
const [selectedNodeId, setSelectedNodeId] = useState<number | null>(null);
  • 状態管理をします。
    • inputValue: 入力フィールドの値を管理します。
    • nodes: ロジックツリー全体のノードを管理します。
    • suggestions: OpenAI APIからの候補を管理します。
    • errorMessage: エラーメッセージを表示するための状態です。
    • addAsChild: ノードを「子として追加」するか「兄弟として追加」するかを管理します。
    • selectedNodeId: 現在選択されているノードのIDを管理します。

4. ノードの選択機能

src/app/page.tsx
const handleNodeClick = (nodeId: number) => {
  setSelectedNodeId(nodeId);
};
  • ノードがクリックされたときに、そのノードのIDを selectedNodeId に設定します。
  • これにより、ユーザーがどのノードを操作しようとしているかをアプリが認識できます。

5. ノードのレンダリング

src/app/page.tsx
const renderTree = (nodes: Node[], level: number = 0) => {
  return (
    <div>
      {nodes.map((node) => (
        <div key={node.id}>
          <div
            onClick={() => handleNodeClick(node.id)}
            style={{
              marginLeft: level * 20,
              backgroundColor:
                node.id === selectedNodeId ? '#e0e0e0' : 'transparent',
              cursor: 'pointer',
            }}
            className="border p-2 m-1"
          >
            {node.content}
          </div>
          {node.children.length > 0 && renderTree(node.children, level + 1)}
        </div>
      ))}
    </div>
  );
};
  • 再帰的にノードをレンダリングし、ツリー構造を表現します。
  • インデントの調整: marginLeft を使って階層に応じたインデントを設定します。
  • 選択状態の表示: 選択されたノードは背景色を変更してハイライトします。
  • クリックイベント: ノードをクリックすると handleNodeClick が呼ばれ、ノードが選択されます。

6. ノードの追加処理

src/app/page.tsx
const handleSubmit = async () => {
  if (!inputValue) return;

  const newNode: Node = {
    id: Date.now(),
    content: inputValue,
    children: [],
  };

  if (selectedNodeId === null) {
    // ルートに追加
    setNodes([...nodes, newNode]);
  } else {
    const selectedNode = findNodeById(nodes, selectedNodeId);
    if (!selectedNode) {
      setErrorMessage('選択されたノードが見つかりません。');
      return;
    }

    if (addAsChild) {
      selectedNode.children.push(newNode);
    } else {
      const parentNode = findParentNode(nodes, selectedNodeId);
      if (parentNode) {
        parentNode.children.push(newNode);
      } else {
        setNodes([...nodes, newNode]);
      }
    }
    setNodes([...nodes]);
  }

  setInputValue('');
  setErrorMessage('');
  setSuggestions([]);
};
  • 入力値の検証: inputValue が空でないか確認します。
  • 新しいノードの作成: 入力された内容を持つ新しいノードを作成します。
  • 論理性のチェック: checkLogic 関数を使って、ノードを追加しても論理的に問題ないか確認します。
  • ノードの追加:
    • ルートに追加: ノードが選択されていない場合、ルートレベルにノードを追加します。
    • 子または兄弟として追加: 選択されたノードに対して、子または兄弟としてノードを追加します。
  • 状態のリセット: ノードが正常に追加された場合、入力フィールドやエラーメッセージをリセットします。

7. ノード検索のヘルパー関数

src/app/page.tsx
const findNodeById = (nodes: Node[], id: number): Node | null => {
  for (const node of nodes) {
    if (node.id === id) return node;
    const childNode = findNodeById(node.children, id);
    if (childNode) return childNode;
  }
  return null;
};

const findParentNode = (
  nodes: Node[],
  id: number,
  parent: Node | null = null
): Node | null => {
  for (const node of nodes) {
    if (node.id === id) return parent;
    const parentNode = findParentNode(node.children, id, node);
    if (parentNode) return parentNode;
  }
  return null;
};
  • findNodeById: ノードのIDを使って、ツリー内から特定のノードを検索します。
  • findParentNode: ノードのIDを使って、その親ノードを検索します。
  • findParentContent: ノードの親ノードの内容(content)を取得します。
  • 再帰的な検索を行うことで、ツリー構造内のどの位置にあるノードでも見つけることができます。

8. 入力フィールドと送信ボタン

src/app/page.tsx
<div className="mb-4">
  <input
    type="text"
    className="border p-2 w-full"
    value={inputValue}
    onChange={(e) => setInputValue(e.target.value)}
    placeholder="テキストを入力してください"
  />
  <button
    onClick={handleSubmit}
    className="bg-blue-500 text-white px-4 py-2 mt-2"
  >
    送信
  </button>
</div>
  • 入力フィールド: ユーザーがノードの内容を入力します。
  • 送信ボタン: 入力された内容をもとに、ノードを追加する処理を開始します。
  • 状態管理: 入力フィールドの値は inputValue で管理され、setInputValue で更新されます。

9. ノードの追加方法の選択

src/app/page.tsx
<div className="mb-4">
  <label className="mr-4">
    <input
      type="radio"
      name="addMethod"
      value="child"
      checked={addAsChild}
      onChange={() => setAddAsChild(true)}
      className="mr-1"
    />
    選択したノードの子として追加
  </label>
  <label>
    <input
      type="radio"
      name="addMethod"
      value="sibling"
      checked={!addAsChild}
      onChange={() => setAddAsChild(false)}
      className="mr-1"
    />
    選択したノードの兄弟として追加
  </label>
</div>
  • ラジオボタンを使用して、ノードを「子として追加」するか「兄弟として追加」するかを選択できます。
  • addAsChild 状態で選択内容を管理し、ノードの追加処理で参照します。

機能解説(大枠)

1. ロジックツリーの構築

  • ツリー構造の再帰的レンダリング: renderTree 関数を使って、ノードとその子ノードを再帰的に表示します。
  • ノードの選択: ノードをクリックすると、そのノードが選択状態になり、追加操作の対象となります。
  • 視覚的な階層表示: インデントやスタイルを調整して、ツリー構造を直感的に理解できるようにします。

2. 論理性のチェック

  • OpenAI APIの活用: ノードを追加する際に、前のノードとの論理的な関係をOpenAI APIを使ってチェックします。
  • チェックの流れ:
    • ユーザーがノードを追加しようとすると、checkLogic 関数が呼ばれます。
    • 前のノード(または親ノード)の内容と新しいノードの内容をAPIに送信します。
    • APIの応答に基づいて、ノードの追加を許可するか、エラーを表示します。

3. エラーメッセージと候補の提示

  • エラーメッセージの表示: 論理的なつながりがない場合、ユーザーに対してエラーメッセージを表示します。
  • 候補の提示: OpenAI APIから受け取った候補をユーザーに提示し、クリックすると入力フィールドに反映されます。
  • ユーザー体験の向上: ユーザーが次に何をすべきか明確に示し、スムーズにアプリを利用できるようにします。


結果


いい感じにロジックツリー機能を作ることができました。





機能改善

OpenAI APIとの統合

1. OpenAI APIの統合

src/app/api/checkLogic/route.ts を作成し、OpenAI APIを使用して論理性のチェックを行います。

src/app/api/checkLogic/route.ts
import { NextResponse } from 'next/server';
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export async function POST(req: Request) {
  const { previousContent, currentContent } = await req.json();

  try {
    const messages = [
      {
        role: 'system',
        content: 'あなたは論理性をチェックするアシスタントです。',
      },
      {
        role: 'user',
        content: `
次の2つの文が論理的に繋がっているか、特に前の文が上位概念(カテゴリー)であり、現在の文がその下位概念や具体的な例である場合、論理的に繋がっていると判断してください。

前の文: ${previousContent}
現在の文: ${currentContent}

もし論理的に繋がっていない場合は、現在の文を基にして論理的に繋がる3つの候補を提案してください。

回答は以下のフォーマットでお願いします。各行の先頭にハイフンとスペースを付けずに、直接 "Valid" または "Invalid" を記載してください。

論理的に繋がっている場合:
"Valid"

論理的に繋がっていない場合:
"Invalid"
"候補1"
"候補2"
"候補3"
`,
      },
    ];

    const response = await openai.chat.completions.create({
      messages,
      model: 'gpt-4',
      max_tokens: 150,
      temperature: 0.7,
    });

    const assistantMessage = response.choices[0]?.message?.content?.trim() ?? '';

    console.log('モデルの応答:', assistantMessage);

    const lines = assistantMessage
      .split('\n')
      .map((line) => line.trim())
      .filter((line) => line.length > 0);

    const firstLine = lines[0]?.replace(/^"|"$/g, '');

    let isValid = false;
    let suggestions: string[] = [];

    if (firstLine === 'Valid') {
      isValid = true;
    } else if (firstLine === 'Invalid') {
      isValid = false;
      suggestions = lines.slice(1).map((line) => line.replace(/^"|"$/g, '').trim());
    }

    return NextResponse.json({ isValid, suggestions });
  } catch (error) {
    console.error('OpenAI APIエラー:', error);
    return NextResponse.json(
      { error: 'OpenAI APIエラーが発生しました' },
      { status: 500 }
    );
  }
}
  • OpenAI APIの初期化: OpenAI インスタンスを作成し、APIキーを設定します。
  • プロンプトの作成:
    • ユーザーからの入力に基づいて、モデルが適切な応答を返せるように詳細なプロンプトを作成します。
  • APIへのリクエスト:
    • openai.chat.completions.create を使用して、モデルにリクエストを送信します。
  • モデルの応答のパース:
    • モデルからの応答を行ごとに分割し、"Valid" または "Invalid" を判定します。
    • suggestions として、提案された候補を配列に格納します。
  • エラーハンドリング:
    • APIの呼び出しでエラーが発生した場合、適切なエラーメッセージを返します。

2. フロントエンドでの論理性チェックの統合

src/app/page.tsx

const checkLogic = async (
  previousContent: string | null,
  currentContent: string
): Promise<boolean> => {
  if (!previousContent) {
    return true;
  }

  try {
    const response = await axios.post('/api/checkLogic', {
      previousContent,
      currentContent,
    });

    const { isValid, suggestions } = response.data;
    if (!isValid) {
      setErrorMessage('入力内容が論理的に繋がっていません。以下の候補から選択してください。');
      setSuggestions(suggestions);
      return false;
    }
    return true;
  } catch (error) {
    console.error('論理性のチェック中にエラーが発生しました:', error);
    setErrorMessage('エラーが発生しました。もう一度お試しください。');
    return false;
  }
};
  • APIへのリクエスト: /api/checkLogic エンドポイントに対して、前のノードの内容と現在のノードの内容を送信します。
  • 応答の処理:
    • isValidfalse の場合、エラーメッセージと候補を設定します。
    • isValidtrue の場合、ノードの追加処理を続行します。
  • エラーハンドリング:
    • リクエスト中にエラーが発生した場合、ユーザーに通知します。

コード改善

1. 候補をクリックして入力フィールドに反映

src/app/page.tsx
const handleSuggestionClick = (suggestion: string) => {
  setInputValue(suggestion);
  setSuggestions([]);
  setErrorMessage('');
};
  • ユーザーが候補をクリックしたときに、その候補を入力フィールドにセットします。
  • 候補リストとエラーメッセージをクリアし、再度送信できる状態にします。

2. 候補の表示

src/app/page.tsx
{suggestions.length > 0 && (
  <div>
    <h2 className="font-bold">候補:</h2>
    <ul>
      {suggestions.map((suggestion, idx) => (
        <li
          key={idx}
          className="list-disc list-inside cursor-pointer text-blue-500 underline"
          onClick={() => handleSuggestionClick(suggestion)}
        >
          {suggestion}
        </li>
      ))}
    </ul>
  </div>
)}
  • 候補リストの表示: suggestions に候補がある場合、リストとして表示します。
  • スタイルの適用:
    • リスト項目をクリック可能にし、視覚的にリンクのように見せます。
  • イベントハンドラ:
    • 候補をクリックすると handleSuggestionClick が呼ばれ、入力フィールドに候補が反映されます。


結果


いい感じに推論とエラー機能を作ることができました。

コード全体

src/app/page.tsx全体(最後の内容まで含む)
src/app/page.tsx
'use client';

import { useState } from 'react';
import axios from 'axios';

interface Node {
  id: number;
  content: string;
  children: Node[];
}

export default function Home() {
  const [inputValue, setInputValue] = useState('');
  const [nodes, setNodes] = useState<Node[]>([]);
  const [suggestions, setSuggestions] = useState<string[]>([]);
  const [errorMessage, setErrorMessage] = useState('');
  const [addAsChild, setAddAsChild] = useState(true);
  const [selectedNodeId, setSelectedNodeId] = useState<number | null>(null);

  const handleNodeClick = (nodeId: number) => {
    setSelectedNodeId(nodeId);
  };

  const handleSubmit = async () => {
    if (!inputValue) return;

    const newNode: Node = {
      id: Date.now(), // 一意のIDを生成
      content: inputValue,
      children: [],
    };

    if (selectedNodeId === null) {
      // ノードが選択されていない場合、ルートに追加
      const isLogical = await checkLogic(null, newNode.content);
      if (isLogical) {
        setNodes([...nodes, newNode]);
        setInputValue('');
        setErrorMessage('');
        setSuggestions([]);
      } else {
        setErrorMessage('入力内容が論理的に繋がっていません。以下の候補から選択してください。');
      }
      return;
    }

    // 選択されたノードを検索
    const selectedNode = findNodeById(nodes, selectedNodeId);
    if (!selectedNode) {
      setErrorMessage('選択されたノードが見つかりません。');
      return;
    }

    // 追加方法に応じて処理
    const previousContent = addAsChild
      ? selectedNode.content
      : findParentContent(nodes, selectedNodeId);

    const isLogical = await checkLogic(previousContent, newNode.content);

    if (isLogical) {
      if (addAsChild) {
        // 子として追加
        selectedNode.children.push(newNode);
      } else {
        // 兄弟として追加
        const parentNode = findParentNode(nodes, selectedNodeId);
        if (parentNode) {
          parentNode.children.push(newNode);
        } else {
          // ルートレベルに追加
          setNodes([...nodes, newNode]);
        }
      }
      setNodes([...nodes]);
      setInputValue('');
      setErrorMessage('');
      setSuggestions([]);
    } else {
      setErrorMessage('入力内容が論理的に繋がっていません。以下の候補から選択してください。');
      // suggestions は checkLogic 関数内で設定されます
    }
  };

  const findNodeById = (nodes: Node[], id: number): Node | null => {
    for (const node of nodes) {
      if (node.id === id) return node;
      const childNode = findNodeById(node.children, id);
      if (childNode) return childNode;
    }
    return null;
  };

  const findParentNode = (
    nodes: Node[],
    id: number,
    parent: Node | null = null
  ): Node | null => {
    for (const node of nodes) {
      if (node.id === id) return parent;
      const parentNode = findParentNode(node.children, id, node);
      if (parentNode) return parentNode;
    }
    return null;
  };

  const findParentContent = (nodes: Node[], id: number): string | null => {
    const parentNode = findParentNode(nodes, id);
    return parentNode ? parentNode.content : null;
  };

  const renderTree = (nodes: Node[], level: number = 0) => {
    return (
      <div>
        {nodes.map((node) => (
          <div key={node.id}>
            <div
              onClick={() => handleNodeClick(node.id)}
              style={{
                marginLeft: level * 20,
                backgroundColor:
                  node.id === selectedNodeId ? '#e0e0e0' : 'transparent',
                cursor: 'pointer',
              }}
              className="border p-2 m-1"
            >
              {node.content}
            </div>
            {node.children.length > 0 && renderTree(node.children, level + 1)}
          </div>
        ))}
      </div>
    );
  };

  const checkLogic = async (
    previousContent: string | null,
    currentContent: string
  ): Promise<boolean> => {
    if (!previousContent) {
      // previousContent が空の場合は論理性のチェックをスキップ
      return true;
    }

    try {
      const response = await axios.post('/api/checkLogic', {
        previousContent,
        currentContent,
      });

      const { isValid, suggestions } = response.data;
      if (!isValid) {
        setErrorMessage('入力内容が論理的に繋がっていません。以下の候補から選択してください。');
        setSuggestions(suggestions); // suggestions をセット
        return false;
      }
      return true;
    } catch (error) {
      console.error('論理性のチェック中にエラーが発生しました:', error);
      setErrorMessage('エラーが発生しました。もう一度お試しください。');
      return false;
    }
  };

  // 候補をクリックして入力フィールドに反映する関数
  const handleSuggestionClick = (suggestion: string) => {
    setInputValue(suggestion);
    setSuggestions([]);
    setErrorMessage('');
  };

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">ロジックツリー作成アプリ</h1>

      {selectedNodeId === null && (
        <p className="text-blue-500">ノードを選択してください。</p>
      )}

      {/* ノードの追加方法を選択 */}
      <div className="mb-4">
        <label className="mr-4">
          <input
            type="radio"
            name="addMethod"
            value="child"
            checked={addAsChild}
            onChange={() => setAddAsChild(true)}
            className="mr-1"
          />
          選択したノードの子として追加
        </label>
        <label>
          <input
            type="radio"
            name="addMethod"
            value="sibling"
            checked={!addAsChild}
            onChange={() => setAddAsChild(false)}
            className="mr-1"
          />
          選択したノードの兄弟として追加
        </label>
      </div>

      {/* 入力フィールドと送信ボタン */}
      <div className="mb-4">
        <input
          type="text"
          className="border p-2 w-full"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="テキストを入力してください"
        />
        <button
          onClick={handleSubmit}
          className="bg-blue-500 text-white px-4 py-2 mt-2"
        >
          送信
        </button>
      </div>

      {errorMessage && <p className="text-red-500">{errorMessage}</p>}
      {suggestions.length > 0 && (
        <div>
          <h2 className="font-bold">候補:</h2>
          <ul>
            {suggestions.map((suggestion, idx) => (
              <li
                key={idx}
                className="list-disc list-inside cursor-pointer text-blue-500 underline"
                onClick={() => handleSuggestionClick(suggestion)}
              >
                {suggestion}
              </li>
            ))}
          </ul>
        </div>
      )}

      {/* ロジックツリーの表示 */}
      <div className="mt-8">
        <h2 className="font-bold mb-2">ロジックツリー:</h2>
        {renderTree(nodes)}
      </div>
    </div>
  );
}

src/app/api/checkLogic/route.ts
src/app/api/checkLogic/route.ts
import { NextResponse } from 'next/server';
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export async function POST(req: Request) {
  const { previousContent, currentContent } = await req.json();

  try {
    const messages: { role: 'system' | 'user' | 'assistant'; content: string }[] = [
      {
        role: 'system',
        content: 'あなたは論理性をチェックするアシスタントです。',
      },
      {
        role: 'user',
        content: `
次の2つの文が論理的に繋がっているか、特に前の文が上位概念(カテゴリー)であり、現在の文がその下位概念や具体的な例である場合、論理的に繋がっていると判断してください。

前の文: ${previousContent}
現在の文: ${currentContent}

もし論理的に繋がっていない場合は、現在の文を基にして論理的に繋がる3つの候補を提案してください。

回答は以下のフォーマットでお願いします。各行の先頭にハイフンとスペースを付けずに、直接 "Valid" または "Invalid" を記載してください。

論理的に繋がっている場合:
"Valid"

論理的に繋がっていない場合:
"Invalid"
"候補1"
"候補2"
"候補3"
`,
      },
    ];

    const response = await openai.chat.completions.create({
      messages,
      model: 'gpt-4o',
      max_tokens: 150,
      temperature: 0.7,
    });

    const assistantMessage = response.choices[0]?.message?.content?.trim() ?? '';

    console.log('モデルの応答:', assistantMessage);

    const lines = assistantMessage
      .split('\n')
      .map((line) => line.trim())
      .filter((line) => line.length > 0);

    const firstLine = lines[0]?.replace(/^"|"$/g, '');

    let isValid = false;
    let suggestions: string[] = [];

    if (firstLine === 'Valid') {
      isValid = true;
    } else if (firstLine === 'Invalid') {
      isValid = false;
      suggestions = lines.slice(1).map((line) => line.replace(/^"|"$/g, '').trim());
    }

    return NextResponse.json({ isValid, suggestions });
  } catch (error) {
    console.error('OpenAI APIエラー:', error);
    return NextResponse.json(
      { error: 'OpenAI APIエラーが発生しました' },
      { status: 500 }
    );
  }
}
.env.local
.env.local
OPENAI_API_KEY=your-openai-api-key

終わりに

お疲れ様でした!今回は、Next.jsOpenAI APIを使って、ロジックツリー作成アプリをゼロから構築しました。
もう少し装飾すればよかったですが、時間がなかったのでまた今度にしようと思います。
あとはバックエンド処理ができればですね。とにかく形はできたので満足です。


Discussion