ロジックツリー作成アプリ(判定付き)
はじめに
皆さん、大学や会社で「論理的に考えろ」と言われたことはありませんか?
私は論理的に考えることがかなり苦手な部類の人間です。今のところ様々なフレームワークを試すことで、改善に努めています。
その一つが「ロジックツリー」。抽象的概念から枝葉を広げていくことで、その事柄における要素の深さ・広さを視覚的に表現することが可能になります。
ですが、私はこの手法を用いていると疑問が浮かびます。
- ノードごとのつながりは、地に足ついてるの?
- 論理の飛躍はないの?
- 他にもっと深掘りできないの?
そのためロジックツリーを視覚化しつつ、論理の整合性を判定できる状態を簡易アプリで実現したいと思います。
目指す要件定義
まずは、今回作成するアプリの要件を明確にしましょう。
- ロジックツリーの作成: ユーザーが入力したテキストをノードとして、ツリー構造で表示します。
- 論理性のチェック: ノードを追加する際、前のノードとの論理的なつながりをOpenAI APIを使ってチェックします。
- ノードの追加方法の選択: ユーザーはノードを「子として追加」または「兄弟として追加」できます。
- 任意のノードへの追加: ツリー内の任意のノードを選択し、そのノードに対して子や兄弟を追加できます。
- エラーメッセージと候補の提示: 論理の飛躍がある場合、エラーメッセージと修正のための候補を表示します。
実装環境
- フロントエンドフレームワーク: Next.js 14
- スタイリング: Tailwind CSS
- プログラミング言語: TypeScript
- API: OpenAI API(GPT-4モデルを使用)
環境準備
1. Next.jsプロジェクトの作成
まずは、Next.jsのプロジェクトを作成します。
npx create-next-app@latest logic-tree-app --typescript --eslint
Need to install the following packages:
create-next-app@14.2.14
Ok to proceed? (y) y
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
Creating a new Next.js app in /Users/username/my-logic-tree-app.
2. ディレクトリに移動
cd logic-tree-app
3. Tailwind CSSのセットアップ
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
を全て上書きします。
@tailwind base;
@tailwind components;
@tailwind utilities;
4. OpenAI APIのインストール
OpenAI APIを使用するため、ライブラリをインストールします。
npm install openai axios
5. 環境変数の設定
プロジェクトのルートに .env.local
ファイルを作成し、OpenAIのAPIキーを設定します。
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全体
'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
を作成し、基本的なページレイアウトを設定します。
'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. ノードのデータ構造を定義
interface Node {
id: number;
content: string;
children: Node[];
}
- ロジックツリーの各ノードを表す Node インターフェースを定義します。
- id: ノードの一意な識別子。
- content: ノードに表示されるテキスト。
- children: 子ノードの配列。
3. 状態管理の追加
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. ノードの選択機能
const handleNodeClick = (nodeId: number) => {
setSelectedNodeId(nodeId);
};
- ノードがクリックされたときに、そのノードのIDを selectedNodeId に設定します。
- これにより、ユーザーがどのノードを操作しようとしているかをアプリが認識できます。
5. ノードのレンダリング
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. ノードの追加処理
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. ノード検索のヘルパー関数
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. 入力フィールドと送信ボタン
<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. ノードの追加方法の選択
<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を使用して論理性のチェックを行います。
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. フロントエンドでの論理性チェックの統合
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
エンドポイントに対して、前のノードの内容と現在のノードの内容を送信します。 -
応答の処理:
-
isValid
がfalse
の場合、エラーメッセージと候補を設定します。 -
isValid
がtrue
の場合、ノードの追加処理を続行します。
-
-
エラーハンドリング:
- リクエスト中にエラーが発生した場合、ユーザーに通知します。
コード改善
1. 候補をクリックして入力フィールドに反映
const handleSuggestionClick = (suggestion: string) => {
setInputValue(suggestion);
setSuggestions([]);
setErrorMessage('');
};
- ユーザーが候補をクリックしたときに、その候補を入力フィールドにセットします。
- 候補リストとエラーメッセージをクリアし、再度送信できる状態にします。
2. 候補の表示
{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全体(最後の内容まで含む)
'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
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
OPENAI_API_KEY=your-openai-api-key
終わりに
お疲れ様でした!今回は、Next.jsとOpenAI APIを使って、ロジックツリー作成アプリをゼロから構築しました。
もう少し装飾すればよかったですが、時間がなかったのでまた今度にしようと思います。
あとはバックエンド処理ができればですね。とにかく形はできたので満足です。
Discussion